GitHub Action commited on
Commit ·
afcd954
1
Parent(s): 7cabfe5
Sync from GitHub with Git LFS
Browse files- scripts/publish_to_hashnode.py +29 -103
scripts/publish_to_hashnode.py
CHANGED
|
@@ -6,188 +6,114 @@ import re
|
|
| 6 |
from pathlib import Path
|
| 7 |
|
| 8 |
import requests
|
| 9 |
-
import markdown
|
| 10 |
-
from markdown.extensions import tables, fenced_code, codehilite, toc
|
| 11 |
|
| 12 |
PUBLISHED_FILE = "published_posts.json"
|
| 13 |
GH_PAGES_BASE = "https://kagvi13.github.io/HMP/"
|
| 14 |
-
HMP_TAGS = ["HMP"] # сюда можно добавлять другие тэги при необходимости
|
| 15 |
|
| 16 |
HASHNODE_TOKEN = os.environ["HASHNODE_TOKEN"]
|
| 17 |
HASHNODE_PUBLICATION_ID = os.environ["HASHNODE_PUBLICATION_ID"]
|
| 18 |
API_URL = "https://gql.hashnode.com"
|
| 19 |
|
| 20 |
-
|
| 21 |
def convert_md_links(md_text: str) -> str:
|
| 22 |
-
"""Конвертирует относительные ссылки (*.md) в абсолютные ссылки на GitHub Pages."""
|
| 23 |
def replacer(match):
|
| 24 |
-
text = match.
|
| 25 |
-
link = match.group(2)
|
| 26 |
if link.startswith("http://") or link.startswith("https://") or not link.endswith(".md"):
|
| 27 |
return match.group(0)
|
| 28 |
abs_link = GH_PAGES_BASE + link.replace(".md", "").lstrip("./")
|
| 29 |
return f"[{text}]({abs_link})"
|
| 30 |
return re.sub(r"\[([^\]]+)\]\(([^)]+)\)", replacer, md_text)
|
| 31 |
|
| 32 |
-
|
| 33 |
def load_published():
|
| 34 |
if Path(PUBLISHED_FILE).exists():
|
| 35 |
with open(PUBLISHED_FILE, "r", encoding="utf-8") as f:
|
| 36 |
return json.load(f)
|
| 37 |
-
print("⚠ published_posts.json не найден — начинаем с нуля.")
|
| 38 |
return {}
|
| 39 |
|
| 40 |
-
|
| 41 |
def save_published(data):
|
| 42 |
with open(PUBLISHED_FILE, "w", encoding="utf-8") as f:
|
| 43 |
json.dump(data, f, ensure_ascii=False, indent=2)
|
| 44 |
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
return hashlib.md5(Path(path).read_bytes()).hexdigest()
|
| 48 |
-
|
| 49 |
|
| 50 |
def graphql_request(query, variables):
|
| 51 |
-
headers = {
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
resp_json = response.json()
|
| 58 |
-
except json.JSONDecodeError:
|
| 59 |
-
raise Exception(f"GraphQL вернул не JSON: {response.text}")
|
| 60 |
-
|
| 61 |
-
print("DEBUG: GraphQL response:", json.dumps(resp_json, indent=2))
|
| 62 |
-
|
| 63 |
-
if response.status_code != 200:
|
| 64 |
-
raise Exception(f"GraphQL request failed with {response.status_code}: {response.text}")
|
| 65 |
-
if "errors" in resp_json:
|
| 66 |
-
raise Exception(f"GraphQL errors: {resp_json['errors']}")
|
| 67 |
-
return resp_json
|
| 68 |
-
|
| 69 |
|
| 70 |
def create_post(title, slug, markdown_content):
|
| 71 |
query = """
|
| 72 |
mutation CreateDraft($input: CreateDraftInput!) {
|
| 73 |
createDraft(input: $input) {
|
| 74 |
-
draft {
|
| 75 |
-
id
|
| 76 |
-
slug
|
| 77 |
-
title
|
| 78 |
-
}
|
| 79 |
}
|
| 80 |
}
|
| 81 |
"""
|
| 82 |
-
variables = {
|
| 83 |
-
|
| 84 |
-
"title": title,
|
| 85 |
-
"contentMarkdown": markdown_content,
|
| 86 |
-
"slug": slug,
|
| 87 |
-
"publicationId": HASHNODE_PUBLICATION_ID,
|
| 88 |
-
"tags": [{"name": tag} for tag in HMP_TAGS] # <-- добавляем теги
|
| 89 |
-
}
|
| 90 |
-
}
|
| 91 |
return graphql_request(query, variables)["data"]["createDraft"]["draft"]
|
| 92 |
|
| 93 |
-
|
| 94 |
-
def update_post(draft_id, title, markdown_content):
|
| 95 |
query = """
|
| 96 |
mutation UpdateDraft($id: ID!, $input: UpdateDraftInput!) {
|
| 97 |
updateDraft(id: $id, input: $input) {
|
| 98 |
-
draft {
|
| 99 |
-
id
|
| 100 |
-
slug
|
| 101 |
-
title
|
| 102 |
-
}
|
| 103 |
}
|
| 104 |
}
|
| 105 |
"""
|
| 106 |
-
variables = {
|
| 107 |
-
"id": draft_id,
|
| 108 |
-
"input": {
|
| 109 |
-
"title": title,
|
| 110 |
-
"contentMarkdown": markdown_content,
|
| 111 |
-
"tags": [{"name": tag} for tag in HMP_TAGS]
|
| 112 |
-
}
|
| 113 |
-
}
|
| 114 |
return graphql_request(query, variables)["data"]["updateDraft"]["draft"]
|
| 115 |
|
| 116 |
-
|
| 117 |
def publish_draft(draft_id):
|
| 118 |
query = """
|
| 119 |
mutation PublishDraft($input: PublishDraftInput!) {
|
| 120 |
-
publishDraft(input: $input) {
|
| 121 |
-
post {
|
| 122 |
-
id
|
| 123 |
-
slug
|
| 124 |
-
url
|
| 125 |
-
}
|
| 126 |
-
}
|
| 127 |
}
|
| 128 |
"""
|
| 129 |
variables = {"input": {"draftId": draft_id}}
|
| 130 |
return graphql_request(query, variables)["data"]["publishDraft"]["post"]
|
| 131 |
|
| 132 |
-
|
| 133 |
def main(force=False):
|
| 134 |
published = load_published()
|
| 135 |
md_files = list(Path("docs").rglob("*.md"))
|
| 136 |
|
| 137 |
for md_file in md_files:
|
| 138 |
name = md_file.stem
|
|
|
|
|
|
|
| 139 |
|
| 140 |
-
|
| 141 |
-
if len(name) < 6:
|
| 142 |
-
title = name + "-HMP"
|
| 143 |
-
else:
|
| 144 |
-
title = name
|
| 145 |
-
|
| 146 |
-
# slug формируем из title, чтобы Hashnode не ругался
|
| 147 |
-
slug = re.sub(r'[^a-z0-9-]', '-', title.lower())
|
| 148 |
-
slug = re.sub(r'-+', '-', slug).strip('-')
|
| 149 |
-
slug = slug[:250]
|
| 150 |
-
|
| 151 |
-
md_text = md_file.read_text(encoding="utf-8")
|
| 152 |
-
source_link = f"Источник: [ {md_file.name} ](https://github.com/kagvi13/HMP/blob/main/docs/{md_file.name})\n\n"
|
| 153 |
-
md_text = source_link + md_text
|
| 154 |
md_text = convert_md_links(md_text)
|
|
|
|
| 155 |
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
# Проверка публикации по title
|
| 160 |
-
if not force and title in published and published[title]["hash"] == h:
|
| 161 |
-
print(f"✅ Пост '{title}' без изменений — пропускаем.")
|
| 162 |
continue
|
| 163 |
|
| 164 |
try:
|
| 165 |
-
if
|
| 166 |
-
|
| 167 |
-
post = update_post(draft_id, title, md_text)
|
| 168 |
print(f"♻ Обновлён пост: https://hashnode.com/@yourusername/{post['slug']}")
|
| 169 |
else:
|
| 170 |
-
|
| 171 |
-
post = publish_draft(
|
| 172 |
print(f"🆕 Пост опубликован: https://hashnode.com/@yourusername/{post['slug']}")
|
| 173 |
|
| 174 |
-
|
| 175 |
-
published[title] = {"id": post["id"], "slug": post["slug"], "hash": h}
|
| 176 |
save_published(published)
|
| 177 |
-
|
| 178 |
-
print("⏱ Пауза 30 секунд перед следующим постом...")
|
| 179 |
time.sleep(30)
|
| 180 |
|
| 181 |
except Exception as e:
|
| 182 |
-
print(f"❌ Ошибка при публикации {
|
| 183 |
save_published(published)
|
| 184 |
break
|
| 185 |
|
| 186 |
-
|
| 187 |
if __name__ == "__main__":
|
| 188 |
import argparse
|
| 189 |
parser = argparse.ArgumentParser()
|
| 190 |
-
parser.add_argument("--force", action="store_true"
|
| 191 |
args = parser.parse_args()
|
| 192 |
-
|
| 193 |
main(force=args.force)
|
|
|
|
| 6 |
from pathlib import Path
|
| 7 |
|
| 8 |
import requests
|
|
|
|
|
|
|
| 9 |
|
| 10 |
PUBLISHED_FILE = "published_posts.json"
|
| 11 |
GH_PAGES_BASE = "https://kagvi13.github.io/HMP/"
|
|
|
|
| 12 |
|
| 13 |
HASHNODE_TOKEN = os.environ["HASHNODE_TOKEN"]
|
| 14 |
HASHNODE_PUBLICATION_ID = os.environ["HASHNODE_PUBLICATION_ID"]
|
| 15 |
API_URL = "https://gql.hashnode.com"
|
| 16 |
|
|
|
|
| 17 |
def convert_md_links(md_text: str) -> str:
|
|
|
|
| 18 |
def replacer(match):
|
| 19 |
+
text, link = match.groups()
|
|
|
|
| 20 |
if link.startswith("http://") or link.startswith("https://") or not link.endswith(".md"):
|
| 21 |
return match.group(0)
|
| 22 |
abs_link = GH_PAGES_BASE + link.replace(".md", "").lstrip("./")
|
| 23 |
return f"[{text}]({abs_link})"
|
| 24 |
return re.sub(r"\[([^\]]+)\]\(([^)]+)\)", replacer, md_text)
|
| 25 |
|
|
|
|
| 26 |
def load_published():
|
| 27 |
if Path(PUBLISHED_FILE).exists():
|
| 28 |
with open(PUBLISHED_FILE, "r", encoding="utf-8") as f:
|
| 29 |
return json.load(f)
|
|
|
|
| 30 |
return {}
|
| 31 |
|
|
|
|
| 32 |
def save_published(data):
|
| 33 |
with open(PUBLISHED_FILE, "w", encoding="utf-8") as f:
|
| 34 |
json.dump(data, f, ensure_ascii=False, indent=2)
|
| 35 |
|
| 36 |
+
def file_hash(md_text: str):
|
| 37 |
+
return hashlib.md5(md_text.encode("utf-8")).hexdigest()
|
|
|
|
|
|
|
| 38 |
|
| 39 |
def graphql_request(query, variables):
|
| 40 |
+
headers = {"Authorization": f"Bearer {HASHNODE_TOKEN}", "Content-Type": "application/json"}
|
| 41 |
+
resp = requests.post(API_URL, json={"query": query, "variables": variables}, headers=headers)
|
| 42 |
+
data = resp.json()
|
| 43 |
+
if "errors" in data:
|
| 44 |
+
raise Exception(f"GraphQL errors: {data['errors']}")
|
| 45 |
+
return data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
def create_post(title, slug, markdown_content):
|
| 48 |
query = """
|
| 49 |
mutation CreateDraft($input: CreateDraftInput!) {
|
| 50 |
createDraft(input: $input) {
|
| 51 |
+
draft { id slug title }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
}
|
| 53 |
}
|
| 54 |
"""
|
| 55 |
+
variables = {"input": {"title": title, "contentMarkdown": markdown_content,
|
| 56 |
+
"slug": slug, "publicationId": HASHNODE_PUBLICATION_ID}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
return graphql_request(query, variables)["data"]["createDraft"]["draft"]
|
| 58 |
|
| 59 |
+
def update_post(post_id, title, markdown_content):
|
|
|
|
| 60 |
query = """
|
| 61 |
mutation UpdateDraft($id: ID!, $input: UpdateDraftInput!) {
|
| 62 |
updateDraft(id: $id, input: $input) {
|
| 63 |
+
draft { id slug title }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
}
|
| 65 |
}
|
| 66 |
"""
|
| 67 |
+
variables = {"id": post_id, "input": {"title": title, "contentMarkdown": markdown_content}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
return graphql_request(query, variables)["data"]["updateDraft"]["draft"]
|
| 69 |
|
|
|
|
| 70 |
def publish_draft(draft_id):
|
| 71 |
query = """
|
| 72 |
mutation PublishDraft($input: PublishDraftInput!) {
|
| 73 |
+
publishDraft(input: $input) { post { id slug url } }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
}
|
| 75 |
"""
|
| 76 |
variables = {"input": {"draftId": draft_id}}
|
| 77 |
return graphql_request(query, variables)["data"]["publishDraft"]["post"]
|
| 78 |
|
|
|
|
| 79 |
def main(force=False):
|
| 80 |
published = load_published()
|
| 81 |
md_files = list(Path("docs").rglob("*.md"))
|
| 82 |
|
| 83 |
for md_file in md_files:
|
| 84 |
name = md_file.stem
|
| 85 |
+
title = name if len(name) >= 6 else name + "-HMP"
|
| 86 |
+
slug = re.sub(r'[^a-z0-9-]', '-', title.lower()).strip('-')[:250]
|
| 87 |
|
| 88 |
+
md_text = f"Источник: [ {md_file.name} ](https://github.com/kagvi13/HMP/blob/main/docs/{md_file.name})\n\n" + md_file.read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
md_text = convert_md_links(md_text)
|
| 90 |
+
h = file_hash(md_text)
|
| 91 |
|
| 92 |
+
if not force and name in published and published[name].get("hash") == h:
|
| 93 |
+
print(f"✅ Пост '{name}' без изменений — пропускаем.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
continue
|
| 95 |
|
| 96 |
try:
|
| 97 |
+
if name in published and "id" in published[name]:
|
| 98 |
+
post = update_post(published[name]["id"], title, md_text)
|
|
|
|
| 99 |
print(f"♻ Обновлён пост: https://hashnode.com/@yourusername/{post['slug']}")
|
| 100 |
else:
|
| 101 |
+
draft = create_post(title, slug, md_text)
|
| 102 |
+
post = publish_draft(draft["id"])
|
| 103 |
print(f"🆕 Пост опубликован: https://hashnode.com/@yourusername/{post['slug']}")
|
| 104 |
|
| 105 |
+
published[name] = {"id": post["id"], "slug": post["slug"], "hash": h}
|
|
|
|
| 106 |
save_published(published)
|
|
|
|
|
|
|
| 107 |
time.sleep(30)
|
| 108 |
|
| 109 |
except Exception as e:
|
| 110 |
+
print(f"❌ Ошибка при публикации {name}: {e}")
|
| 111 |
save_published(published)
|
| 112 |
break
|
| 113 |
|
|
|
|
| 114 |
if __name__ == "__main__":
|
| 115 |
import argparse
|
| 116 |
parser = argparse.ArgumentParser()
|
| 117 |
+
parser.add_argument("--force", action="store_true")
|
| 118 |
args = parser.parse_args()
|
|
|
|
| 119 |
main(force=args.force)
|