github-rest-api 0.49.0__tar.gz → 0.51.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/.github/workflows/auto_merge_pull_request.yaml +5 -22
- github_rest_api-0.51.1/.github/workflows/check_workflow_template.yaml +20 -0
- github_rest_api-0.51.1/.github/workflows/claude.yaml +27 -0
- github_rest_api-0.51.1/.github/workflows/create_pr_dev_to_main.yaml +9 -0
- github_rest_api-0.51.1/.github/workflows/create_pr_to_dev.yaml +11 -0
- github_rest_api-0.51.1/.github/workflows/create_pr_to_main.yaml +10 -0
- github_rest_api-0.51.1/.github/workflows/pr-workflow.yaml +34 -0
- github_rest_api-0.51.1/.github/workflows/release.yaml +12 -0
- github_rest_api-0.51.1/.github/workflows/remove_branch.yaml +9 -0
- github_rest_api-0.49.0/GEMINI.md → github_rest_api-0.51.1/AGENTS.md +1 -1
- github_rest_api-0.51.1/CLAUDE.md +1 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/PKG-INFO +3 -3
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/github_rest_api/github.py +26 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/github_rest_api/scripts/github/create_github_repo.py +117 -47
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/github_rest_api/scripts/github/release_on_github.py +16 -12
- github_rest_api-0.51.1/github_rest_api/scripts/github/upload_image_to_issue.py +533 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/github_rest_api/scripts/utils.py +77 -1
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/pyproject.toml +7 -2
- github_rest_api-0.51.1/tests/test_create_github_repo.py +326 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/tests/test_github.py +68 -0
- github_rest_api-0.51.1/tests/test_release_on_github.py +121 -0
- github_rest_api-0.51.1/tests/test_upload_image_to_issue.py +701 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/tests/test_utils.py +36 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/uv.lock +2 -2
- github_rest_api-0.49.0/.github/workflows/claude-code-review.yml +0 -67
- github_rest_api-0.49.0/.github/workflows/claude.yml +0 -50
- github_rest_api-0.49.0/.github/workflows/create_pr_to_main.yaml +0 -20
- github_rest_api-0.49.0/.github/workflows/lint.yaml +0 -34
- github_rest_api-0.49.0/.github/workflows/release.yaml +0 -22
- github_rest_api-0.49.0/.github/workflows/remove_branch.yaml +0 -18
- github_rest_api-0.49.0/.github/workflows/test.yaml +0 -27
- github_rest_api-0.49.0/github_rest_api/scripts/github/workflows/auto_merge_pull_request.yaml +0 -30
- github_rest_api-0.49.0/github_rest_api/scripts/github/workflows/create_pr_dev_to_main.yaml +0 -19
- github_rest_api-0.49.0/github_rest_api/scripts/github/workflows/create_pr_to_dev.yaml +0 -21
- github_rest_api-0.49.0/github_rest_api/scripts/github/workflows/create_pr_to_main.yaml +0 -20
- github_rest_api-0.49.0/github_rest_api/scripts/github/workflows/python/lint.yaml +0 -27
- github_rest_api-0.49.0/github_rest_api/scripts/github/workflows/python/test.yaml +0 -31
- github_rest_api-0.49.0/github_rest_api/scripts/github/workflows/remove_branch.yaml +0 -18
- github_rest_api-0.49.0/tests/test_create_github_repo.py +0 -128
- github_rest_api-0.49.0/tests/test_release_on_github.py +0 -64
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/.devcontainer/devcontainer.json +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/.gitignore +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/README.md +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/github_rest_api/__init__.py +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/github_rest_api/pr_content.py +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/github_rest_api/scripts/__init__.py +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/github_rest_api/scripts/cargo/__init__.py +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/github_rest_api/scripts/cargo/benchmark.py +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/github_rest_api/scripts/cargo/profiling.py +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/github_rest_api/scripts/cargo/utils.py +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/github_rest_api/scripts/container/__init__.py +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/github_rest_api/scripts/container/build_container_images.py +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/github_rest_api/scripts/container/config_container.py +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/github_rest_api/scripts/container/update_version_containerfile.py +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/github_rest_api/scripts/github/__init__.py +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/github_rest_api/scripts/github/auto_merge_pull_request.py +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/github_rest_api/scripts/github/create_pull_request.py +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/github_rest_api/scripts/github/remove_branch.py +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/github_rest_api/utils.py +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/memory/MEMORY.md +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/memory/feedback_test_runner.md +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/tests/__init__.py +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/tests/test_auto_merge_pull_request.py +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/tests/test_build_container_images.py +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/tests/test_create_pull_request.py +0 -0
- {github_rest_api-0.49.0 → github_rest_api-0.51.1}/tests/test_pr_content.py +0 -0
{github_rest_api-0.49.0 → github_rest_api-0.51.1}/.github/workflows/auto_merge_pull_request.yaml
RENAMED
|
@@ -13,9 +13,7 @@ on:
|
|
|
13
13
|
# Renaming any of these silently disables the trigger.
|
|
14
14
|
workflow_run:
|
|
15
15
|
workflows:
|
|
16
|
-
-
|
|
17
|
-
- Lint Code
|
|
18
|
-
- Run Tests
|
|
16
|
+
- PR Workflow
|
|
19
17
|
types:
|
|
20
18
|
- completed
|
|
21
19
|
# Kept for approvals that come from a human rather than from Claude.
|
|
@@ -60,22 +58,7 @@ jobs:
|
|
|
60
58
|
if: |
|
|
61
59
|
github.event_name != 'workflow_run'
|
|
62
60
|
|| github.event.workflow_run.conclusion == 'success'
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
steps:
|
|
68
|
-
- name: Install uv
|
|
69
|
-
run: |
|
|
70
|
-
curl -LsSf https://astral.sh/uv/install.sh | sudo env UV_INSTALL_DIR="/usr/local/bin" sh
|
|
71
|
-
- name: Auto Merge PR
|
|
72
|
-
# `--allowed-types` is intentionally left at the upstream default
|
|
73
|
-
# (build, chore, ci, docs, deps) so that only simple PRs auto-merge;
|
|
74
|
-
# anything substantive, `feat` and `fix` included, is merged by hand.
|
|
75
|
-
run: |
|
|
76
|
-
uvx --from github-rest-api@latest auto_merge_pull_request \
|
|
77
|
-
--authors dclong \
|
|
78
|
-
--approvers dclong 'claude[bot]' 'gemini-code-assist[bot]' \
|
|
79
|
-
--marker 'AUTO_MERGE_APPROVED' \
|
|
80
|
-
${{ inputs.dry_run && '--dry-run' || '' }} \
|
|
81
|
-
--token ${{ secrets.ACTIONS_TOKEN }}
|
|
61
|
+
uses: legendu-net/.github/.github/workflows/auto_merge_pull_request.yaml@main
|
|
62
|
+
with:
|
|
63
|
+
dry_run: ${{ inputs.dry_run || false }}
|
|
64
|
+
secrets: inherit
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Check Workflow Templates
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
paths:
|
|
6
|
+
- .github/workflows/**
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
check_workflow_template:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v6
|
|
13
|
+
- name: Check workflow templates use .yaml extension
|
|
14
|
+
run: |
|
|
15
|
+
yml_files=$(find .github/workflows/ -name '*.yml')
|
|
16
|
+
if [ -n "$yml_files" ]; then
|
|
17
|
+
echo "The following workflow templates must use the .yaml extension instead of .yml:"
|
|
18
|
+
echo "$yml_files"
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Claude Code
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
issue_comment:
|
|
5
|
+
types: [created]
|
|
6
|
+
pull_request_review_comment:
|
|
7
|
+
types: [created]
|
|
8
|
+
issues:
|
|
9
|
+
types: [opened, assigned]
|
|
10
|
+
pull_request_review:
|
|
11
|
+
types: [submitted]
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
claude:
|
|
15
|
+
if: |
|
|
16
|
+
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
|
17
|
+
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
|
18
|
+
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
|
19
|
+
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
|
20
|
+
permissions:
|
|
21
|
+
actions: read
|
|
22
|
+
contents: read
|
|
23
|
+
issues: read
|
|
24
|
+
pull-requests: read
|
|
25
|
+
id-token: write
|
|
26
|
+
uses: legendu-net/.github/.github/workflows/claude.yml@main
|
|
27
|
+
secrets: inherit
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: PR Workflow
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, synchronize, ready_for_review, reopened]
|
|
6
|
+
branches:
|
|
7
|
+
- dev
|
|
8
|
+
- main
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
lint_code:
|
|
12
|
+
uses: legendu-net/.github/.github/workflows/lint.yaml@main
|
|
13
|
+
secrets: inherit
|
|
14
|
+
|
|
15
|
+
test_code:
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
pull-requests: read
|
|
19
|
+
uses: legendu-net/.github/.github/workflows/test.yaml@main
|
|
20
|
+
secrets: inherit
|
|
21
|
+
|
|
22
|
+
claude-review:
|
|
23
|
+
needs: [lint_code, test_code]
|
|
24
|
+
permissions:
|
|
25
|
+
contents: read
|
|
26
|
+
issues: write
|
|
27
|
+
pull-requests: write
|
|
28
|
+
id-token: write
|
|
29
|
+
uses: legendu-net/.github/.github/workflows/claude-code-review.yml@main
|
|
30
|
+
with:
|
|
31
|
+
pr_number: ${{ github.event.pull_request.number }}
|
|
32
|
+
draft: ${{ github.event.pull_request.draft }}
|
|
33
|
+
head_repo_full_name: ${{ github.event.pull_request.head.repo.full_name }}
|
|
34
|
+
secrets: inherit
|
|
@@ -7,7 +7,7 @@ A simple Python wrapper for GitHub REST APIs, optimized for use in GitHub Action
|
|
|
7
7
|
- **Purpose:** Provide a streamlined interface for interacting with GitHub's REST API
|
|
8
8
|
and performing Git operations within automation scripts.
|
|
9
9
|
- **Main Technologies:**
|
|
10
|
-
- **Python 3.12+**: Core language.
|
|
10
|
+
- **Python 3.12.7+**: Core language.
|
|
11
11
|
- **requests**: For HTTP interactions with the GitHub API.
|
|
12
12
|
- **dulwich**: A pure-Python implementation of Git for repository operations.
|
|
13
13
|
- **tenacity**: For retry logic on API requests.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
AGENTS.md
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: github-rest-api
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.51.1
|
|
4
4
|
Summary: Simple wrapper of GitHub REST APIs.
|
|
5
5
|
Author-email: Ben Du <longendu@yahoo.com>
|
|
6
6
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
7
7
|
Classifier: Programming Language :: Python :: 3.12
|
|
8
8
|
Classifier: Programming Language :: Python :: 3.13
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.14
|
|
10
|
-
Requires-Python: <4,>=3.12
|
|
10
|
+
Requires-Python: <4,>=3.12.7
|
|
11
11
|
Requires-Dist: dulwich>=0.25.1
|
|
12
12
|
Requires-Dist: psutil>=5.9.4
|
|
13
13
|
Requires-Dist: pynacl>=1.5
|
|
@@ -1465,6 +1465,32 @@ class Repository(GitHub):
|
|
|
1465
1465
|
json={"body": body},
|
|
1466
1466
|
).json()
|
|
1467
1467
|
|
|
1468
|
+
def create_issue_comment_image(
|
|
1469
|
+
self, issue_number: int, url: str, alt: str = "", body: str = ""
|
|
1470
|
+
) -> dict[str, Any]:
|
|
1471
|
+
"""Add a comment containing an image to an issue.
|
|
1472
|
+
|
|
1473
|
+
GitHub has no public API for the attachment upload that its web UI
|
|
1474
|
+
performs on drag and drop, so the image must already be hosted
|
|
1475
|
+
somewhere reachable; this method only references it.
|
|
1476
|
+
|
|
1477
|
+
:param issue_number: The number of the issue.
|
|
1478
|
+
:param url: The URL of an already uploaded image.
|
|
1479
|
+
:param alt: Alt text of the image.
|
|
1480
|
+
:param body: Optional text placed above the image.
|
|
1481
|
+
"""
|
|
1482
|
+
# A bracket in the alt text would close the markdown label early and
|
|
1483
|
+
# degrade the whole image to literal text, so escape both brackets
|
|
1484
|
+
# (and the escape character itself, which must come first).
|
|
1485
|
+
alt = alt.replace("\\", "\\\\").replace("[", "\\[").replace("]", "\\]")
|
|
1486
|
+
# The URL is wrapped in angle brackets so that spaces and unbalanced
|
|
1487
|
+
# parentheses, which are common in URLs derived from file names, do
|
|
1488
|
+
# not terminate the markdown link early.
|
|
1489
|
+
image = f""
|
|
1490
|
+
return self.create_issue_comment(
|
|
1491
|
+
issue_number, f"{body}\n\n{image}" if body else image
|
|
1492
|
+
)
|
|
1493
|
+
|
|
1468
1494
|
def archive(self) -> requests.Response:
|
|
1469
1495
|
return self._patch(
|
|
1470
1496
|
url=self._url_repo,
|
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
"""Add a GitHub repository and initialize a local Git repository with workflows."""
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
-
import getpass
|
|
5
4
|
import logging
|
|
6
|
-
import os
|
|
7
|
-
import shutil
|
|
8
5
|
import sys
|
|
9
6
|
from collections.abc import Sequence
|
|
10
7
|
from pathlib import Path
|
|
11
8
|
|
|
9
|
+
import requests
|
|
12
10
|
from dulwich import porcelain
|
|
13
11
|
from dulwich.refs import HEADREF, LOCAL_BRANCH_PREFIX, Ref
|
|
14
12
|
|
|
15
13
|
from github_rest_api import Organization, User
|
|
14
|
+
from github_rest_api.github import DEFAULT_TIMEOUT, URL_API, build_http_headers
|
|
15
|
+
from github_rest_api.scripts.utils import resolve_github_token, validate_repo
|
|
16
16
|
from github_rest_api.utils import as_str_sequence
|
|
17
17
|
|
|
18
18
|
logger = logging.getLogger(__name__)
|
|
19
19
|
|
|
20
|
+
# Workflows that configure this repository itself rather than the repositories
|
|
21
|
+
# created from these templates.
|
|
22
|
+
_REPO_ONLY_WORKFLOWS = frozenset({"check_workflow_template.yaml"})
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if len(parts) != 2 or not parts[0] or not parts[1]:
|
|
24
|
-
raise ValueError(f"Invalid repo format '{repo}'. Expected 'owner/repo'.")
|
|
24
|
+
# The default GitHub repo to copy workflow templates from.
|
|
25
|
+
DEFAULT_WORKFLOWS_REPO = "legendu-net/github_rest_api"
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
def _create_remote_repo(
|
|
@@ -45,11 +46,29 @@ def _remote_url(repo: str, protocol: str) -> str:
|
|
|
45
46
|
|
|
46
47
|
|
|
47
48
|
def _ensure_remote(path: Path, repo: str, protocol: str) -> None:
|
|
49
|
+
url = _remote_url(repo, protocol)
|
|
48
50
|
try:
|
|
49
|
-
porcelain.remote_add(path, "origin",
|
|
51
|
+
porcelain.remote_add(path, "origin", url)
|
|
50
52
|
logger.info("Added remote 'origin' for %s", repo)
|
|
51
53
|
except porcelain.RemoteExists:
|
|
52
54
|
pass
|
|
55
|
+
# `porcelain.remote_add` only writes the remote's `url`, unlike `git remote
|
|
56
|
+
# add`/`git clone`, which also set a `fetch` refspec. Without it, `git
|
|
57
|
+
# fetch`/`jj git fetch` never update `refs/remotes/origin/*`, so local
|
|
58
|
+
# branches silently stop tracking the remote. Repair it for a pre-existing
|
|
59
|
+
# 'origin' too (e.g. added by an older version of this script), but only
|
|
60
|
+
# when it already points at this repo, so an unrelated 'origin' remote is
|
|
61
|
+
# left untouched.
|
|
62
|
+
with porcelain.open_repo_closing(path) as r:
|
|
63
|
+
c = r.get_config()
|
|
64
|
+
try:
|
|
65
|
+
existing_url = c.get((b"remote", b"origin"), b"url")
|
|
66
|
+
except KeyError:
|
|
67
|
+
return
|
|
68
|
+
if existing_url != url.encode():
|
|
69
|
+
return
|
|
70
|
+
c.set((b"remote", b"origin"), b"fetch", b"+refs/heads/*:refs/remotes/origin/*")
|
|
71
|
+
c.write_to_path()
|
|
53
72
|
|
|
54
73
|
|
|
55
74
|
def _active_branch(path: Path) -> str:
|
|
@@ -80,12 +99,12 @@ def _head_and_branches(path: Path) -> tuple[str | None, set[str]]:
|
|
|
80
99
|
|
|
81
100
|
def _init_local_repo(
|
|
82
101
|
repo: str,
|
|
83
|
-
language: str,
|
|
84
102
|
dir_: str,
|
|
85
103
|
token: str,
|
|
86
104
|
protocol: str,
|
|
87
105
|
push: bool,
|
|
88
106
|
branches: Sequence[str] = ("main",),
|
|
107
|
+
workflows_repo: str = DEFAULT_WORKFLOWS_REPO,
|
|
89
108
|
) -> None:
|
|
90
109
|
branches = list(dict.fromkeys(branches))
|
|
91
110
|
if not branches:
|
|
@@ -154,64 +173,114 @@ def _init_local_repo(
|
|
|
154
173
|
logger.info("Successfully pushed branch '%s'.", branch)
|
|
155
174
|
else:
|
|
156
175
|
logger.info("Skipping push (pass --push to push branches to the remote).")
|
|
157
|
-
_add_workflow(path,
|
|
176
|
+
_add_workflow(path, workflows_repo=workflows_repo, token=token)
|
|
158
177
|
|
|
159
178
|
|
|
160
179
|
def create_github_repo(
|
|
161
180
|
repo: str,
|
|
162
181
|
private: bool,
|
|
163
|
-
language: str,
|
|
164
182
|
is_owner_user: bool,
|
|
165
183
|
dir_: str,
|
|
166
184
|
token: str,
|
|
167
185
|
protocol: str,
|
|
168
186
|
push: bool,
|
|
169
187
|
branches: Sequence[str] = ("main",),
|
|
188
|
+
workflows_repo: str = DEFAULT_WORKFLOWS_REPO,
|
|
170
189
|
) -> None:
|
|
171
190
|
branches = as_str_sequence(branches)
|
|
172
|
-
token = token
|
|
173
|
-
if not token:
|
|
174
|
-
token = getpass.getpass("Please enter your GitHub token: ")
|
|
175
|
-
if not token:
|
|
176
|
-
raise ValueError(
|
|
177
|
-
"No GitHub token is provided (via $GITHUB_TOKEN, --token or at prompt)."
|
|
178
|
-
)
|
|
191
|
+
token = resolve_github_token(token)
|
|
179
192
|
repo = repo.strip()
|
|
180
|
-
|
|
193
|
+
validate_repo(repo)
|
|
181
194
|
_create_remote_repo(
|
|
182
195
|
repo=repo, private=private, token=token, is_owner_user=is_owner_user
|
|
183
196
|
)
|
|
184
197
|
_init_local_repo(
|
|
185
198
|
repo=repo,
|
|
186
|
-
language=language,
|
|
187
199
|
dir_=dir_,
|
|
188
200
|
token=token,
|
|
189
201
|
branches=branches,
|
|
190
202
|
protocol=protocol,
|
|
191
203
|
push=push,
|
|
204
|
+
workflows_repo=workflows_repo,
|
|
192
205
|
)
|
|
193
206
|
|
|
194
207
|
|
|
195
|
-
def
|
|
196
|
-
|
|
197
|
-
|
|
208
|
+
def _list_workflow_entries(workflows_repo: str, token: str) -> list[dict]:
|
|
209
|
+
"""List the workflow template file entries available in a GitHub repo.
|
|
210
|
+
|
|
211
|
+
:param workflows_repo: The GitHub repo (in the format of owner/repo) to
|
|
212
|
+
list workflow templates from.
|
|
213
|
+
:param token: The GitHub token used to authenticate the request.
|
|
214
|
+
"""
|
|
215
|
+
url = f"{URL_API}/repos/{workflows_repo}/contents/.github/workflows"
|
|
216
|
+
resp = requests.get(
|
|
217
|
+
url=url, headers=build_http_headers(token), timeout=DEFAULT_TIMEOUT
|
|
218
|
+
)
|
|
219
|
+
resp.raise_for_status()
|
|
220
|
+
return [
|
|
221
|
+
entry
|
|
222
|
+
for entry in resp.json()
|
|
223
|
+
if entry["type"] == "file"
|
|
224
|
+
and entry["name"].endswith(".yaml")
|
|
225
|
+
and entry["name"] not in _REPO_ONLY_WORKFLOWS
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _download_workflow(workflows_repo: str, entry: dict, token: str) -> str:
|
|
230
|
+
"""Download the raw content of a single workflow template.
|
|
231
|
+
|
|
232
|
+
Prefers the entry's ``download_url`` (served from
|
|
233
|
+
raw.githubusercontent.com, which has a much higher rate limit than the
|
|
234
|
+
api.github.com contents endpoint used to list the entries), falling back
|
|
235
|
+
to a raw content request against the contents API for the rare case
|
|
236
|
+
where GitHub omits it.
|
|
237
|
+
|
|
238
|
+
:param workflows_repo: The GitHub repo (in the format of owner/repo) to
|
|
239
|
+
download the workflow template from.
|
|
240
|
+
:param entry: A file entry as returned by ``_list_workflow_entries``.
|
|
241
|
+
:param token: The GitHub token used to authenticate the request.
|
|
242
|
+
"""
|
|
243
|
+
download_url = entry.get("download_url")
|
|
244
|
+
if download_url:
|
|
245
|
+
# No Authorization header here: raw.githubusercontent.com already
|
|
246
|
+
# embeds any access it needs (e.g. a signed token for a private repo)
|
|
247
|
+
# in the URL itself, and forwarding an Authorization header scoped to
|
|
248
|
+
# a *different* repo makes it fail closed with a 404 even for public
|
|
249
|
+
# content.
|
|
250
|
+
resp = requests.get(url=download_url, timeout=DEFAULT_TIMEOUT)
|
|
251
|
+
else:
|
|
252
|
+
url = f"{URL_API}/repos/{workflows_repo}/contents/.github/workflows/{
|
|
253
|
+
entry['name']
|
|
254
|
+
}"
|
|
255
|
+
headers = build_http_headers(token) | {
|
|
256
|
+
"Accept": "application/vnd.github.raw+json"
|
|
257
|
+
}
|
|
258
|
+
resp = requests.get(url=url, headers=headers, timeout=DEFAULT_TIMEOUT)
|
|
259
|
+
resp.raise_for_status()
|
|
260
|
+
return resp.text
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _add_workflow(path: Path, workflows_repo: str, token: str) -> None:
|
|
264
|
+
"""Download workflow templates from a GitHub repo into a local repo.
|
|
265
|
+
|
|
266
|
+
:param path: The local repo directory to add workflow templates to.
|
|
267
|
+
:param workflows_repo: The GitHub repo (in the format of owner/repo) to
|
|
268
|
+
copy workflow templates from. An empty string skips this step.
|
|
269
|
+
:param token: The GitHub token used to authenticate the requests.
|
|
270
|
+
"""
|
|
271
|
+
if not workflows_repo:
|
|
272
|
+
logger.info("Skipping workflow templates (workflows_repo is empty).")
|
|
273
|
+
return
|
|
274
|
+
entries = _list_workflow_entries(workflows_repo, token)
|
|
275
|
+
if not entries:
|
|
276
|
+
raise FileNotFoundError(f"No workflow templates found in '{workflows_repo}'.")
|
|
198
277
|
dir_dest = path / ".github" / "workflows"
|
|
199
278
|
dir_dest.mkdir(parents=True, exist_ok=True)
|
|
200
|
-
for
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return
|
|
206
|
-
lang_dir = workflow_dir / language
|
|
207
|
-
if not lang_dir.exists():
|
|
208
|
-
return
|
|
209
|
-
for yaml in lang_dir.glob("*.yaml"):
|
|
210
|
-
if not (dir_dest / yaml.name).exists():
|
|
211
|
-
shutil.copy2(yaml, dir_dest)
|
|
212
|
-
logger.info(
|
|
213
|
-
"Copied %s specific workflow %s to %s", language, yaml.name, dir_dest
|
|
214
|
-
)
|
|
279
|
+
for entry in entries:
|
|
280
|
+
dest = dir_dest / entry["name"]
|
|
281
|
+
if not dest.exists():
|
|
282
|
+
dest.write_text(_download_workflow(workflows_repo, entry, token))
|
|
283
|
+
logger.info("Downloaded workflow %s from %s", entry["name"], workflows_repo)
|
|
215
284
|
|
|
216
285
|
|
|
217
286
|
def parse_args(args=None, namespace=None):
|
|
@@ -250,14 +319,6 @@ def parse_args(args=None, namespace=None):
|
|
|
250
319
|
action="store_false",
|
|
251
320
|
help="Whether to create the repository as public.",
|
|
252
321
|
)
|
|
253
|
-
parser.add_argument(
|
|
254
|
-
"-l",
|
|
255
|
-
"--lang",
|
|
256
|
-
"--language",
|
|
257
|
-
dest="language",
|
|
258
|
-
default="",
|
|
259
|
-
help="The language of the GitHub repository.",
|
|
260
|
-
)
|
|
261
322
|
parser.add_argument(
|
|
262
323
|
"-d",
|
|
263
324
|
"--dir",
|
|
@@ -287,6 +348,15 @@ def parse_args(args=None, namespace=None):
|
|
|
287
348
|
action="store_true",
|
|
288
349
|
help="Push branches to the remote (by default, nothing is pushed).",
|
|
289
350
|
)
|
|
351
|
+
parser.add_argument(
|
|
352
|
+
"-w",
|
|
353
|
+
"--workflows-repo",
|
|
354
|
+
dest="workflows_repo",
|
|
355
|
+
default=DEFAULT_WORKFLOWS_REPO,
|
|
356
|
+
help="The GitHub repo (in the format of owner/repo) to copy workflow "
|
|
357
|
+
f"templates from (default: {DEFAULT_WORKFLOWS_REPO}). "
|
|
358
|
+
"Pass an empty string to skip copying workflow templates.",
|
|
359
|
+
)
|
|
290
360
|
parser.add_argument(
|
|
291
361
|
"-v",
|
|
292
362
|
"--verbose",
|
|
@@ -304,13 +374,13 @@ def main() -> int:
|
|
|
304
374
|
create_github_repo(
|
|
305
375
|
repo=args.repo,
|
|
306
376
|
private=args.private,
|
|
307
|
-
language=args.language,
|
|
308
377
|
is_owner_user=args.is_owner_user,
|
|
309
378
|
dir_=args.dir,
|
|
310
379
|
token=args.token,
|
|
311
380
|
branches=args.branches,
|
|
312
381
|
protocol=args.protocol,
|
|
313
382
|
push=args.push,
|
|
383
|
+
workflows_repo=args.workflows_repo,
|
|
314
384
|
)
|
|
315
385
|
except Exception as e:
|
|
316
386
|
logger.error("Failed to create GitHub repo: %s", e, exc_info=args.verbose)
|
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
import getpass
|
|
3
|
-
import os
|
|
4
1
|
import re
|
|
5
2
|
import sys
|
|
6
3
|
from pathlib import Path
|
|
7
4
|
|
|
8
5
|
from github_rest_api import Repository
|
|
9
6
|
from github_rest_api.scripts.utils import (
|
|
7
|
+
RedactingArgumentParser,
|
|
10
8
|
find_project_root,
|
|
11
9
|
get_project_version,
|
|
12
10
|
get_repo,
|
|
11
|
+
reject_github_token,
|
|
12
|
+
resolve_github_token,
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def _get_release_tag(tag: str, root: Path, validate: bool = True) -> str:
|
|
17
|
+
reject_github_token("--tag", tag)
|
|
17
18
|
tag = tag.strip()
|
|
18
19
|
if not tag:
|
|
19
20
|
tag = get_project_version(root).strip()
|
|
@@ -54,7 +55,14 @@ def release_on_github(
|
|
|
54
55
|
:param token: GitHub token.
|
|
55
56
|
If not specified, the GITHUB_TOKEN environment variable is used.
|
|
56
57
|
:param validate: If True, validate the tag against semantic versioning format before creating the release.
|
|
58
|
+
:raises ValueError: If a value that looks like a GitHub token is specified
|
|
59
|
+
for anything other than the token itself.
|
|
57
60
|
"""
|
|
61
|
+
# A release is public, so a token mistyped into any of these would be
|
|
62
|
+
# published. -t is the tag on this script rather than the token, which
|
|
63
|
+
# makes that slip easy; --no-validate would otherwise let it through.
|
|
64
|
+
reject_github_token("--branch", branch)
|
|
65
|
+
reject_github_token("--notes", notes)
|
|
58
66
|
root = find_project_root()
|
|
59
67
|
if not root:
|
|
60
68
|
raise FileNotFoundError("Could not find project root (no .git found).")
|
|
@@ -65,13 +73,7 @@ def release_on_github(
|
|
|
65
73
|
if not repo_name:
|
|
66
74
|
raise ValueError("Could not find GitHub repository name.")
|
|
67
75
|
|
|
68
|
-
token = token
|
|
69
|
-
if not token:
|
|
70
|
-
token = getpass.getpass("Please enter your GitHub token: ")
|
|
71
|
-
if not token:
|
|
72
|
-
raise ValueError(
|
|
73
|
-
"No GitHub token is provided (via $GITHUB_TOKEN, --token or at prompt)."
|
|
74
|
-
)
|
|
76
|
+
token = resolve_github_token(token)
|
|
75
77
|
repo = Repository(token=token, repo=repo_name)
|
|
76
78
|
data = {
|
|
77
79
|
"tag_name": tag,
|
|
@@ -87,7 +89,7 @@ def release_on_github(
|
|
|
87
89
|
|
|
88
90
|
|
|
89
91
|
def parse_args(args=None, namespace=None):
|
|
90
|
-
parser =
|
|
92
|
+
parser = RedactingArgumentParser(
|
|
91
93
|
description="Make a release of the project on GitHub."
|
|
92
94
|
)
|
|
93
95
|
parser.add_argument(
|
|
@@ -97,7 +99,9 @@ def parse_args(args=None, namespace=None):
|
|
|
97
99
|
help="The branch (default to main) from which to make the release.",
|
|
98
100
|
)
|
|
99
101
|
parser.add_argument(
|
|
100
|
-
|
|
102
|
+
# No short option on purpose: -t reads as the token to anyone used to
|
|
103
|
+
# the other scripts, and the tag becomes the public name of the
|
|
104
|
+
# release, so that slip would publish the token.
|
|
101
105
|
"--tag",
|
|
102
106
|
default="",
|
|
103
107
|
help="The tag for the release. If not specified, the version from project configuration is used.",
|