Publishing HUGO Post to DevTo Using GitHub CI and CD Pipeline
@ rushi | Saturday, Nov 6, 2021 | 3 minutes read | Update at Saturday, Nov 6, 2021

Why this tool?

  • There are aleady a few github actions available like publish-devto and markdown-to-devto They seem to be failing despite of the valid data in the markdown file.
  • For a large amount of posts there was no option to add a break and the API got overflowed.

Steps to set up

Creating API Keys in Dev.To

Login to dev.to and then go to accounts and create new api key.

Paste the key as DEVTO_TOKEN in Github Secrets

Set up the path for the Posts in devpush.py

By default it is content/posts/*.md

Script to push to dev.to

devpush.py

import json
import requests
import frontmatter
import glob
import os
from time import sleep
import re

'''
Hugo to DevTo
This script transforms a Hugo article into a format that can be pushed to the DevTo server.
'''

URL = "https://dev.to/api/articles"

ALL_PREVIOUS_ARTICLES = requests.get(
            url=URL+"/me/all",
            headers={"api_key": os.environ["DEVTO_TOKEN"]},
        ).json()

def check_if_article_exists(article):
    for a in ALL_PREVIOUS_ARTICLES:
        if a["title"] == article.title:
            # print(a)
            return a['id']
    return None


class HugoArticle(object):
    def assign_if_not_none(self, x, ind=None):
        if x is not None and ind is None:
            return x
        elif x is not None and ind is not None and ind in x:
            return self.assign_if_not_none(x[ind])
        else:
            return ""
    
    def compare_already_existing_articles(self, title, body_markdown, tags):
        # Set a flag no_change to skip POST/ PUT request if article already exists
        for a in ALL_PREVIOUS_ARTICLES:
            if "title" in a and "body_markdown" in a and a["title"] == title and  a["body_markdown"] == body_markdown:
                return True
        return False

    def __init__(self, article, published=False, series=None):
        # Get title, text and tags from hugo markdown files
        self.title = self.assign_if_not_none(article.metadata, "title")

        # Set this to truw if you want to post and publish the article without saving as draft
        self.published = published

        body_markdown = self.assign_if_not_none(article.content)
        relative_image_paths = re.findall("\!\[(.*?)\]\(\/img\/(.*?)\)", body_markdown)
        for relative_image_path in relative_image_paths:
            image = os.path.join(
                "https://raw.githubusercontent.com/rushichaudhari/rushichaudhari.github.io/main/static/img",
                relative_image_path[1],
            )
            body_markdown = body_markdown.replace(relative_image_path[1], image)

        self.body_markdown = body_markdown
        self.tags = self.assign_if_not_none(article.metadata, "tags")

        # assign categories as tags if tags is none
        if self.tags is None:
            self.tags = self.assign_if_not_none(article.metadata, "categories")

        self.series = series

        self.no_change = self.compare_already_existing_articles(self.title, self.body_markdown, self.tags)


def get_article_from_file(filepath):
    with open(filepath, "r") as f:
        article = frontmatter.load(f)
        return HugoArticle(article)


if __name__ == "__main__":
    print("Starting devpush")
    files = glob.glob('content/posts/*.md')

    sleeptime = 5
    print('DEVTO_TOKEN is ', os.environ["DEVTO_TOKEN"], os.getcwd(), files)
    for file in files:
        hugo_article = get_article_from_file(file)

        if not hugo_article.no_change:
            sleep(int(sleeptime))

            this_dict = {"article": hugo_article.__dict__}
            data = json.dumps(this_dict)
            print(data) 

            existing_post_id = check_if_article_exists(hugo_article)
            if existing_post_id is not None:
                url=URL+"/"+str(existing_post_id)
                result = requests.put(
                url=url,
                json=json.loads(data),
                headers={"api_key": os.environ["DEVTO_TOKEN"]},
                )
            else:
                result = requests.post(
                    url=URL,
                    json=json.loads(data),
                    headers={"api_key": os.environ["DEVTO_TOKEN"]},
                )
            print(file)
        else:
            print(file, "no change")

The workflow file will be something like

name: publish
on:
  push:
    branches:
      - master

jobs:
  publish:
    runs-on: ubuntu-latest
    if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"   # Skip CI if the commit message has [skip ci]

    steps:
    - uses: actions/checkout@v2

    - name: setup python
      uses: actions/setup-python@v2
      with:
        python-version: 3.8 #install the python needed
    
    - name: Install python libs
      run: pip3 install requests python-frontmatter

    - name: execute py script # run the run.py to get the latest data
      run: |
        python ./.github/devpush.py #make sure to give the path to the devpush.py file
      env:
        DEVTO_TOKEN: ${{ secrets.DEVTO_TOKEN }}
        SLEEP_TIME: ${{ secrets.SLEEP_TIME }}
        MARKDOWN_POSTS_PATH: ${{secrets.MARKDOWN_POSTS_PATH}}

This post has ben published using the above script

It will appear as a draft, to directly publish it change the published flag to True in the hugo article.

Constraints

  • This script handles conversion of relative paths to raw.githubusercontent paths, to do this all the relative image paths should start with
/img and not like ../../static/img

e.g. 

![](/img/sdsjkd/sdsd/sdsd/sdsdgdffgd/dfgfdg/dfdf.anyformat)

Future plans?

Go decentralized and make a copy with CI CD in dev.to/github/medium, Stay tuned :)

Resources

关于我

g1eny0ung 的 ❤️ 博客

记录一些 🌈 生活上,技术上的事

一名大四学生

马上(已经)毕业于 🏫 大连东软信息学院

职业是前端工程师

业余时间会做开源和 Apple App (OSX & iOS)

主要的技术栈是:

  • JavaScript & TypeScript
  • React.js
  • Electron
  • Rust

写着玩(写过):

  • Java & Clojure & CLJS
  • OCaml & Reason & ReScript
  • Dart & Swift

目前在 PingCAP 工作

– 2020 年 09 月 09 日更新

其他

如果你喜欢我的开源项目或者它们可以给你带来帮助,可以赏一杯咖啡 ☕ 给我。~

If you like my open source projects or they can help you. You can buy me a coffee ☕.~

PayPal

https://paypal.me/g1eny0ung

Patreon:

Become a Patron!

微信赞赏码

wechat

最好附加一下信息或者留言,方便我可以将捐助记录 📝 下来,十分感谢 🙏。

It is better to attach some information or leave a message so that I can record the donation 📝, thank you very much 🙏.