File size: 5,537 Bytes
7403ab9
713516b
7403ab9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b23c183
713516b
 
 
 
 
 
7403ab9
 
8e54bfd
 
7403ab9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import re
import time

import gradio as gr
from huggingface_hub import CommitOperationAdd, HfApi, RepoUrl, SpaceCard

IMPORTS_REGEX = re.compile(r"^(from|import) .*$", re.MULTILINE)

PR_DESCRIPTION = """
This PR enables Space CI on your Space. **Gradio Space CI is a tool to create ephemeral Spaces for each PR opened on your Space repo.** The goal is to improve developer experience by making the review process as lean as possible.

### ⚙️ How it works:

- Listens to pull requests events:
  - If PR is opened => starts an ephemeral Space
  - If PR is updated => updates the Space
  - If PR is closed => deleted the Space
- Checks PR author:
  - If trusted author => ephemeral Space is configured with variables, secrets and hardware.
  - If not a trusted author => ephemeral Space is started without configuration.
  - Space owners are trusted by default. Additional "trusted authors" can be configuration manually.

### ⚠️ Before merging:

1. Check that the configuration is correct. By default the Space is configured to run ephemeral Spaces on a (free) CPU instance without any secrets.
2. You must set `HF_TOKEN` as a secret in your Space settings. Token must have 'write' permission. You can create a new one in your [User settings](https://huggingface.co/settings/token).

---
This is an automated PR created with https://huggingface.co/spaces/Wauplin/gradio-space-ci.
For more details about Space CI, checkout [this page]](https://huggingface.co/spaces/Wauplin/gradio-space-ci/blob/main/README.md).
If you find any issues, please report here: https://huggingface.co/spaces/Wauplin/gradio-space-ci/discussions

Feel free to ignore this PR.
"""

SUCCESS_MESSAGE = """
### Success 🔥

Yay! A PR has been open to enable Space CI on {space_id}. Check it out here: [{pr_url}]({pr_url}).

You can contact the Space owner to let them know about this PR.
"""


def open_pr(space_id_or_url: str, oauth_token: gr.OAuthToken | None) -> str:
    if oauth_token is None:
        raise gr.Error("You must be logged in to open a PR. Click on 'Sign in with Huggingface' first.")
    token = oauth_token.token
    if oauth_token.expires_at > time.time():
        raise gr.Error("Token expired. Logout and try again.")

    api = HfApi(token=token)

    if not space_id:
        raise gr.Error("You must input a Space ID or URL.")
    space_id = (
        RepoUrl(space_id_or_url).repo_id if "https://huggingface.co/spaces/" in space_id_or_url else space_id_or_url
    )

    # 0. Check token + repo existence
    try:
        user = api.whoami()
        if user["type"] != "user":
            raise gr.Error(
                "You must use a user token, not an organization token. Go to https://huggingface.co/settings/token to create a new token."
            )
    except Exception as e:
        raise gr.Error("Invalid token: {e}") from e

    if not api.repo_exists(space_id, repo_type="space"):
        raise gr.Error(f"Space does not exist: 'https://huggingface.co/spaces/{space_id}'.")

    # 1. Add to requirements.txt
    if api.file_exists(repo_id=space_id, repo_type="space", filename="requirements.txt"):
        requirements_file = api.hf_hub_download(repo_id=space_id, repo_type="space", filename="requirements.txt")
        with open(requirements_file) as f:
            requirements = f.read()
    else:
        requirements = ""
    if "gradio-space-ci" not in requirements:
        requirements += "\ngradio-space-ci @ git+https://huggingface.co/spaces/Wauplin/gradio-space-ci@0.2.1\n"

    # 2. Configure CI in README.md
    card = SpaceCard.load(api.hf_hub_download(repo_id=space_id, repo_type="space", filename="README.md"))
    card.data["space_ci"] = {
        "trusted_authors": [],
        "secrets": [],
        "hardware": "cpu-basic",
        "storage": None,
    }
    if card.data.sdk != "gradio":
        raise gr.Error(f"Space must be a Gradio Space, not '{card.data.sdk}'.")
    app_file = card.data.app_file
    if app_file is None:
        raise gr.Error("Space must have an app_file defined their README.")
    if not api.file_exists(repo_id=space_id, repo_type="space", filename=app_file):
        raise gr.Error(f"Could not find app file '{app_file}' in Space repo.")

    # 3. Enable CI in app.py
    with open(api.hf_hub_download(repo_id=space_id, repo_type="space", filename=app_file)) as f:
        app = f.read()
    if "enable_space_ci()" in app:
        raise gr.Error("Space CI is already enabled.")

    all_imports = list(IMPORTS_REGEX.finditer(app))
    if len(all_imports) == 0:
        raise gr.Error("Could not find any imports in app.py.")
    last_import = all_imports[-1]

    app = (
        app[: last_import.end()]
        + "\n\n"
        + "from gradio_space_ci import enable_space_ci"
        + "\n\n"
        + "enable_space_ci()"
        + "\n\n"
        + app[last_import.end() :]
    )

    # 4. Push changes as a PR
    commit = api.create_commit(
        repo_id=space_id,
        repo_type="space",
        operations=[
            CommitOperationAdd(path_in_repo="README.md", path_or_fileobj=str(card).encode()),
            CommitOperationAdd(path_in_repo="requirements.txt", path_or_fileobj=requirements.encode()),
            CommitOperationAdd(path_in_repo=app_file, path_or_fileobj=app.encode()),
        ],
        commit_message="Enable Space CI",
        commit_description=PR_DESCRIPTION,
        create_pr=True,
    )
    assert commit.pr_url is not None  # since `create_pr=True`

    return SUCCESS_MESSAGE.format(space_id=space_id, pr_url=commit.pr_url)