{"schemaVersion":"1.0","type":"TechArticle","types":["Article","TechArticle"],"slug":"publish-from-git-to-zyvop-with-github-actions-c93ut","url":"https://zyvop.com/publish-from-git-to-zyvop-with-github-actions-c93ut","title":"Publish from Git to ZyVOP with GitHub Actions","subtitle":"Write ZyVOP posts in Markdown, check them on pull requests, and publish on merge with GitHub Actions. A step-by-step guide with working workflows.","tldr":"Keep every ZyVOP article as a Markdown file in Git and publish it when you merge. This guide covers the token, frontmatter, the official GitHub Action, a CLI alternative, pull request dry-runs, and the habits that prevent duplicate posts.","keywords":["blogging","GitHub Actions","cicd","DevOps","markdown","Tutorial","Building ZyVOP in Public"],"entities":["Sanju Singh","blogging","GitHub Actions","cicd","DevOps","markdown","Tutorial","Building ZyVOP in Public","ZyVOP"],"keyTakeaways":["You can keep every ZyVOP article as a Markdown file in Git and let GitHub Actions publish it when you merge to main.","No copy-pasting, and every change goes through a pull request.","Why publish from Git Treating posts like code gives you things a web editor can't: Review and history."],"headings":["Why publish from Git","What you need","Step 1: Add a post with frontmatter","Step 2: Store your token as a secret","Step 3: Add the workflow","Alternative: call the CLI yourself","Step 4: Check posts on pull requests","Habits that keep it safe","Troubleshooting","Wrap-up","Further reading"],"outboundLinks":["https://github.com/zyvop/publish-action","https://github.com/zyvop/zyvop-cli"],"contentText":"You can keep every ZyVOP article as a Markdown file in Git and let GitHub Actions publish it when you merge to main. No copy-pasting, and every change goes through a pull request. Why publish from Git Treating posts like code gives you things a web editor can't: Review and history. Edits go through pull requests, and git log becomes your editorial history. One source of truth. The Markdown file is the article. ZyVOP can cross-post it to Dev.to, Hashnode, Medium, WordPress and Bluesky, with canonical URLs handled for you. Safe re-runs. Give each post a stable slug, zyvop_id or canonical_url, and later pushes update the existing article instead of creating another one. What you need A ZyVOP account, with any destinations you want (Dev.to, Hashnode and so on) connected in your settings. A developer token. Generate one in ZyVOP under Settings → Integrations; tokens start with zv_. A GitHub repository with a posts/ folder. Step 1: Add a post with frontmatter Each post is a Markdown file with YAML frontmatter at the top. A simple repository layout looks like this: my-blog/ ├── .github/ │ └── workflows/ │ └── publish.yml └── posts/ └── nodejs-logging-pino-vs-winston.mdAnd the post itself: --- title: The Best Logging Library for Node.js in 2026 slug: nodejs-logging-pino-vs-winston subtitle: How to choose between Pino, Winston and the rest excerpt: A practical comparison of Node.js logging libraries, with code samples. tags: [nodejs, logging, pino, winston, backend] category: backend draft: true cross_post: devto: true hashnode: true medium: false --- Write the article here in plain Markdown, with code blocks, tables and diagrams.Two defaults matter. A post is published unless you say otherwise, and cross-posting is switched on for every connected platform. That is why the example starts with draft: true. The slug matters just as much: it is how a later push finds the post it should update. These are the frontmatter fields you will use most: Field What it does title Headline (required, 3 to 200 characters) slug, zyvop_id Stable identifiers that let later pushes update this post instead of creating another subtitle Short line shown under the title excerpt Card preview and search snippet tags Topic tags, as a list or a comma-separated string category Category slug, such as backend, frontend, devops or ai cover_image Cover image URL or relative path canonical_url Original source URL, if the post lives elsewhere first; also identifies the post on later publishes cross_post Per-platform flags: devto, hashnode, medium, bluesky, wordpress draft, status, scheduled_at Save as a draft, or set DRAFT, PUBLISHED or SCHEDULED with an ISO 8601 date Extra keys such as layout or featured are preserved and ignored by ZyVOP, so the same files can also feed a static site built with Astro or Next.js. Step 2: Store your token as a secret In your GitHub repository, open Settings → Secrets and variables → Actions and create a repository secret named ZYVOP_TOKEN with your developer token as the value. Never commit the token itself. Step 3: Add the workflow The simplest option is ZyVOP's own action. Save this as .github/workflows/publish.yml: name: Publish with ZyVOP on: push: branches: [main] paths: - \"posts/**/*.md\" workflow_dispatch: permissions: contents: read jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: zyvop/publish-action@v1 env: ZYVOP_TOKEN: ${{ secrets.ZYVOP_TOKEN }}The action publishes only the Markdown files changed by a push. Checkout uses fetch-depth: 0 so the base commit is available; when it isn't, or on a manual run (workflow_dispatch) or a branch's first push, the action processes every post. It reads your token from the ZYVOP_TOKEN secret, and once you merge a change to main the run shows up under your repository's Actions tab. Alternative: call the CLI yourself If you want to own the changed-file logic, run the zyvop CLI yourself. It needs Node.js 20 or newer. This version, adapted from ZyVOP's CLI documentation, publishes only the files changed since the previous push, and falls back to all posts on a branch's first push or a force push: name: Publish with ZyVOP CLI on: push: branches: [main] paths: - \"posts/**.md\" permissions: contents: read # Do not let rapid pushes publish the same article concurrently. concurrency: group: zyvop-publish-${{ github.ref }} cancel-in-progress: false jobs: publish: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 - name: Detect changed posts and publish env: ZYVOP_TOKEN: ${{ secrets.ZYVOP_TOKEN }} run: | set -euo pipefail before=\"${{ github.event.before }}\" after=\"${{ github.sha }}\" # A branch's first push has an all-zero before SHA. A force push can # also reference an unavailable commit, so publish all tracked posts. if [[ \"$before\" =~ ^0+$ ]] || ! git cat-file -e \"$before^{commit}\" 2&gt;/dev/null; then mapfile -d '' files &lt; &lt;(git ls-files -z -- ':(glob)posts/**/*.md') else mapfile -d '' files &lt; &lt;( git diff --name-only -z --diff-filter=ACMR \\ \"$before\" \"$after\" -- ':(glob)posts/**/*.md' ) fi if (( ${#files[@]} == 0 )); then echo \"No Markdown files changed.\" exit 0 fi for file in \"${files[@]}\"; do if [ -f \"$file\" ]; then echo \"Deploying $file...\" npx --yes zyvop@1.1.1 publish \"$file\" fi doneTwo notes. The workflow only publishes added or modified files, so deleting a post from Git doesn't remove it from ZyVOP. And if you would rather keep your Dev.to, Hashnode, Medium, WordPress or Bluesky credentials in GitHub secrets than in ZyVOP, the CLI has a --local mode that publishes to those providers directly from the runner. Its documentation warns that Medium's API can't update a published story, so repeated local publishes to Medium can create another one. Step 4: Check posts on pull requests The CLI's --dry-run flag validates a post and previews the exact payload without authentication or publishing, which makes it a cheap check for every pull request. It runs locally, so it can't tell you whether a real publish would create or update the post. name: Check posts on: pull_request: paths: - 'posts/**' permissions: contents: read jobs: dry-run: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - name: Validate every post run: | set -euo pipefail mapfile -d '' files &lt; &lt;(git ls-files -z -- ':(glob)posts/**/*.md') for file in \"${files[@]}\"; do echo \"Checking $file\" npx --yes zyvop@1.1.1 publish \"$file\" --dry-run doneHabits that keep it safe Start every post as a draft. Keep draft: true until the pull request that should make it live, and flip it in that PR. A first push or a manual run processes every post, so mark any back catalogue as drafts too. Give every post a stable identity. Set a slug, zyvop_id or canonical_url once and keep it stable, so later pushes update the same article. Schedule instead of racing. Set status: SCHEDULED and a scheduled_at timestamp to release a post later. Mind the rate limit. ZyVOP documents 10 requests per minute per token on its REST API, so when you move a large back catalogue, publish it in small batches. Pin the CLI version. ZyVOP's own examples pin it (zyvop@1.1.1 at the time of writing), so a new release can't change your pipeline unannounced. Protect the token. Keep it in GitHub secrets only. If it ever leaks, revoke it in ZyVOP and create a new one. Set base_url for relative images. ZyVOP resolves relative cover and body image paths against it, or against canonical_url. Troubleshooting The workflow doesn't run. Check that the branch is main and that the changed files match the paths filter. Every post is republished on each run. Check that checkout uses fetch-depth: 0. When the base commit isn't available, the action processes all tracked posts. A post was created twice. Add a stable slug, zyvop_id or canonical_url so ZyVOP can match the file to the existing article. Authentication fails. Confirm the secret is named ZYVOP_TOKEN and that the token hasn't been revoked. A platform didn't get the post. Connect that destination in ZyVOP and check the cross_post flags, because ZyVOP only syndicates to connected platforms. Wrap-up With one folder, one secret and one workflow file, publishing becomes a side effect of merging. Start with a draft, run the dry-run check on pull requests, and set draft to false only when the post is ready. Further reading ZyVOP Publish Action ZyVOP CLI ZyVOP developer API and CLI documentation ZyVOP AI agents and MCP server","contentHash":"sha256:2b4664fe5c9245bf6ab7b7bacaea968451055992fa35f78472f679573eeac960","authorName":"Sanju Singh","authorUrl":"https://zyvop.com/author/sanjay687","authorSameAs":[],"category":"Tutorial","tags":["blogging","GitHub Actions","cicd","DevOps","markdown"],"audience":"Software engineers and developers building applications with Tutorial","tone":"Instructional, practical, code-first","readingTimeMinutes":6,"wordCount":1350,"faqs":null,"primaryTopic":"Tutorial","publishedAt":"2026-10-17T06:12:44.405Z","updatedAt":"2026-10-17T06:12:44.405Z","canonicalUrl":"https://zyvop.com/publish-from-git-to-zyvop-with-github-actions-c93ut"}