GitHub Action
commited on
Commit
·
12ac8ff
1
Parent(s):
021b49f
Sync from GitHub with Git LFS
Browse files- 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)
|