GitHub Action commited on
Commit
12ac8ff
·
1 Parent(s): 021b49f

Sync from GitHub with Git LFS

Browse files
Files changed (1) hide show
  1. scripts/publish_to_hashnode.py +157 -0
scripts/publish_to_hashnode.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import hashlib
4
+ import time
5
+ 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
+
15
+ HASHNODE_TOKEN = os.environ["HASHNODE_TOKEN"]
16
+ HASHNODE_PUBLICATION_ID = os.environ["HASHNODE_PUBLICATION_ID"]
17
+ API_URL = "https://gql.hashnode.com"
18
+
19
+
20
+ def convert_md_links(md_text: str) -> str:
21
+ """Конвертирует относительные ссылки (*.md) в абсолютные ссылки на GitHub Pages."""
22
+ def replacer(match):
23
+ text = match.group(1)
24
+ link = match.group(2)
25
+ if link.startswith("http://") or link.startswith("https://") or not link.endswith(".md"):
26
+ return match.group(0)
27
+ abs_link = GH_PAGES_BASE + link.replace(".md", "").lstrip("./")
28
+ return f"[{text}]({abs_link})"
29
+ return re.sub(r"\[([^\]]+)\]\(([^)]+)\)", replacer, md_text)
30
+
31
+
32
+ def load_published():
33
+ if Path(PUBLISHED_FILE).exists():
34
+ with open(PUBLISHED_FILE, "r", encoding="utf-8") as f:
35
+ return json.load(f)
36
+ print("⚠ published_posts.json не найден — начинаем с нуля.")
37
+ return {}
38
+
39
+
40
+ def save_published(data):
41
+ with open(PUBLISHED_FILE, "w", encoding="utf-8") as f:
42
+ json.dump(data, f, ensure_ascii=False, indent=2)
43
+
44
+
45
+ def file_hash(path):
46
+ return hashlib.md5(Path(path).read_bytes()).hexdigest()
47
+
48
+
49
+ def graphql_request(query, variables):
50
+ headers = {"Authorization": HASHNODE_TOKEN}
51
+ response = requests.post(API_URL, json={"query": query, "variables": variables}, headers=headers)
52
+ if response.status_code != 200:
53
+ raise Exception(f"GraphQL request failed with {response.status_code}: {response.text}")
54
+ return response.json()
55
+
56
+
57
+ def create_post(title, slug, html):
58
+ query = """
59
+ mutation CreatePost($input: CreateStoryInput!) {
60
+ createStory(input: $input) {
61
+ post {
62
+ _id
63
+ slug
64
+ url
65
+ }
66
+ }
67
+ }
68
+ """
69
+ variables = {
70
+ "input": {
71
+ "title": title,
72
+ "slug": slug,
73
+ "contentMarkdown": html,
74
+ "isPartOfPublication": {
75
+ "publicationId": HASHNODE_PUBLICATION_ID
76
+ }
77
+ }
78
+ }
79
+ return graphql_request(query, variables)["data"]["createStory"]["post"]
80
+
81
+
82
+ def update_post(post_id, title, slug, html):
83
+ query = """
84
+ mutation UpdatePost($id: ID!, $input: UpdateStoryInput!) {
85
+ updateStory(id: $id, input: $input) {
86
+ post {
87
+ _id
88
+ slug
89
+ url
90
+ }
91
+ }
92
+ }
93
+ """
94
+ variables = {
95
+ "id": post_id,
96
+ "input": {
97
+ "title": title,
98
+ "slug": slug,
99
+ "contentMarkdown": html
100
+ }
101
+ }
102
+ return graphql_request(query, variables)["data"]["updateStory"]["post"]
103
+
104
+
105
+ def main(force=False):
106
+ published = load_published()
107
+ md_files = list(Path("docs").rglob("*.md"))
108
+
109
+ for md_file in md_files:
110
+ name = md_file.stem
111
+ slug = name.lower().replace("_", "-")
112
+ h = file_hash(md_file)
113
+
114
+ if not force and name in published and published[name]["hash"] == h:
115
+ print(f"✅ Пост '{name}' без изменений — пропускаем.")
116
+ continue
117
+
118
+ md_text = md_file.read_text(encoding="utf-8")
119
+ source_link = f"Источник: [ {md_file.name} ](https://github.com/kagvi13/HMP/blob/main/docs/{md_file.name})\n\n"
120
+ md_text = source_link + md_text
121
+ md_text = convert_md_links(md_text)
122
+
123
+ # Hashnode принимает Markdown, так что HTML-конвертация не обязательна
124
+ # Но мы можем оставить HTML для единообразия
125
+ html_content = markdown.markdown(
126
+ md_text,
127
+ extensions=["tables", "fenced_code", "codehilite", "toc"]
128
+ )
129
+
130
+ try:
131
+ if name in published and "id" in published[name]:
132
+ post_id = published[name]["id"]
133
+ post = update_post(post_id, name, slug, md_text)
134
+ print(f"♻ Обновлён пост: {post['url']}")
135
+ else:
136
+ post = create_post(name, slug, md_text)
137
+ print(f"🆕 Пост опубликован: {post['url']}")
138
+
139
+ published[name] = {"id": post["_id"], "slug": post["slug"], "hash": h}
140
+ save_published(published)
141
+
142
+ print("⏱ Пауза 30 секунд перед следующим постом...")
143
+ time.sleep(30)
144
+
145
+ except Exception as e:
146
+ print(f"❌ Ошибка при публикации {name}: {e}")
147
+ save_published(published)
148
+ break
149
+
150
+
151
+ if __name__ == "__main__":
152
+ import argparse
153
+ parser = argparse.ArgumentParser()
154
+ parser.add_argument("--force", action="store_true", help="Обновить все посты, даже без изменений")
155
+ args = parser.parse_args()
156
+
157
+ main(force=args.force)