Skip to main content

Release-to-Blog Automation

Β· 8 min read

Overview​

When an app release goes out, a blog post is published automatically listing every task that belongs to that release.

Each post contains:

  • Release notes (from the GitHub release)
  • All resolved tasks from NPS-Support, grouped into Bug fixes / Tasks / Improvements
  • A link back to the GitHub release
  • Installation instructions

When does it trigger?​

Two triggers are wired in the app repo workflow (notify-release.yml):

#TriggerWhen it firesPreferred
1workflow_run on " Publish To AppSource"after AL-Go finishes publishing to AppSource successfullyβœ… yes
2release: publishedas soon as a GitHub release is published (AL-Go " Create release")fallback

Both are enabled. Whichever fires first creates the post; the second run is a no-op because the script never overwrites an existing post.

Note: AL-Go workflow names begin with a leading space (" Publish To AppSource"). workflow_run.workflows matches on the exact name, so that space must be kept.

How a task is linked to a release​

There is no milestone convention in NPS-Support, so a task belongs to a release when both hold:

  1. Label matches the app. Each app in doc/apps-config.json has an explicit labels array listing the real NPS-Support labels that belong to it. Matching is normalised (lowercase, non-alphanumerics stripped), and repoName / shortCode / displayName act as extra fallbacks.

    The explicit list matters because the real labels are inconsistent β€” and one even has a typo that has to be matched as-is:

    Actual label in NPS-SupportApp
    LOC, Localization featuresBusiness-Central-Localization
    PyR, HRM and PayrollHRM-and-Payroll
    EINElectronic-Invoicing
    TRO, Travel ExpensesTravel-Order
    ELC, Electonic Invoicing and Localization Connector (sic)CON_EIN_NPSLoc
    ESPElectronic-Shipments
    POSPoint-Of-Sale-Retail

    Adding a new app or label? Add it to labels in apps-config.json β€” nothing else needs to change.

  2. Closed inside the release window β€” between the previous release's published_at and this release's published_at. When there is no previous release it falls back to FALLBACK_DAYS (default 90).

Setup​

1. Secrets​

SecretWhereWhy
DOCS_REPO_TOKENeach app repoPAT (repo scope) to dispatch to the docs repo
NPS_SUPPORT_TOKENnpsbeograd.github.ioPAT (repo scope) to read NPS-Support issues β€” the default GITHUB_TOKEN cannot read another repository

2. Add the workflow to each app repository​

Copy .github/workflows/EXAMPLE-app-repo-notify-release.yml into each app repo as:

.github/workflows/notify-release.yml

3. Label issues in NPS-Support​

Give each issue the app label β€” repoName, shortCode, or displayName all work (matching is normalised and case-insensitive).

Nightly sync​

.github/workflows/nightly-sync.yml runs every night at 01:00 UTC (02:00 CET / 03:00 CEST) and does three things in one commit:

  1. fetch-releases.js β€” refreshes doc/src/data/releases.json, the build-time data behind the /releases page (every published release across the whole organisation).
  2. sync-recent-releases.js β€” creates a blog post for any release from the last 7 days that does not have one yet. This is the safety net: if a repository_dispatch was missed, the nightly run backfills it.
  3. sync-issues-to-blog.js β€” creates posts for tasks closed in the last 30 days.

All three are idempotent β€” nothing is overwritten, so re-running is safe.

Manual run: Actions β†’ Nightly Sync (releases + blog) β†’ Run workflow (release_days, issue_days are adjustable).

Empty releases are skipped​

Most AL-Go releases are housekeeping (Update AL-Go System Files, Incremented Version number) with no user-facing tasks. A post for those is noise, so a release with zero linked tasks is skipped. Force one with SKIP_EMPTY=false.

Posts are dated by when the release shipped, not when the post was generated, so a backfill keeps the blog chronology correct.

The /releases page​

A standalone changelog at /releases lists every release across the organisation β€” search, filter by product, and two views (by product / timeline). It reads the committed src/data/releases.json, so there are no runtime API calls and no rate limits.

What each release shows is the list of resolved issues (not PRs β€” the app repositories are private, so PR links would be dead ends). Issues come from two places, exactly as in sync-release-to-blog.js: the app's own repository, plus NPS-Support issues carrying the app's label. Both are linked; links into private repositories only resolve for people with access, but the titles are readable by everyone.

  • Maintenance releases are hidden by default. A release with zero resolved issues is CI housekeeping (version bump, AL-Go system files). A toggle shows how many are hidden and reveals them.
  • Every release has a permalink β€” /releases#<product>-<tag>, e.g. /releases#hrm-payroll-28-0-15. Opening such a link switches to the product view, expands the product, reveals maintenance releases if needed, opens the row and scrolls to it. The # handle appears on hover at the left of a row.
  • Latest is marked per product (the newest release of each app), not once for the whole organisation.
  • Apps with no published release do not appear; they show up automatically after their first release, since the nightly job rediscovers repositories.

Publishing (going live)​

Committing content does not change the live site by itself β€” the site is static and served from the gh-pages branch. .github/workflows/deploy.yml builds and publishes it:

  • on every push to back-up that touches doc/
  • after any sync workflow finishes successfully (bot commits made with GITHUB_TOKEN never trigger push, so this is what puts nightly content live)
  • manually: Actions β†’ Deploy to GitHub Pages β†’ Run workflow

It writes CNAME (docs.nps.rs) into the published branch every time, and doc/static/CNAME covers the manual npm run deploy path β€” without a CNAME file GitHub Pages can drop the custom domain after a deploy.

Highlights for notable releases (hand-written)​

Automation gives every release its list of resolved issues. For releases that deserve a sentence written for customers β€” a new module, a major change β€” add an entry to doc/src/data/highlights.json:

"HRM-and-Payroll@28.0.14": {
"title": "New payroll calculation engine",
"summary": "Handles multiple positions in the same period. No setup changes required."
}

Key is <repoName>@<tag>. The nightly sync merges it into the release data; the release then shows a Highlight pill and the note on /releases, and the site announcement uses the title instead of the generic issue count. Keys starting with _ are ignored (the file ships with an example).

Data layout​

FileContents
src/data/releases.jsonlight index β€” every release with taskCount and highlight, no issue lists
src/data/tasks/<repo>.json{ "<tag>": [issues] } β€” loaded by the page only when that product is opened
src/data/highlights.jsonhand-written highlights (see above)

Both generated files are rewritten by fetch-releases.js; only highlights.json is edited by hand.

Site-wide "new release" notice​

The announcement bar is computed at build time in docusaurus.config.js (latestReleaseAnnouncement()): it shows the newest release that has resolved issues, linking to its permalink on /releases. Its id contains the release, so a visitor who dismissed the bar sees it again when the next release ships. Maintenance-only releases are never announced.

Where the logic lives​

FilePurpose
doc/scripts/fetch-releases.jspulls all org releases β†’ src/data/releases.json
doc/scripts/sync-recent-releases.jsensures recent releases have a blog post (backfill)
doc/scripts/sync-release-to-blog.jsbuilds a single release post (single source of truth)
doc/scripts/sync-issues-to-blog.jsbuilds per-issue update posts
doc/src/pages/releases.jsthe /releases changelog page
.github/workflows/nightly-sync.ymlnightly: releases data + both kinds of posts
.github/workflows/sync-release-to-blog.ymlreceives the dispatch, runs the release script
.github/workflows/sync-issues-to-blog.ymlruns the issues script when an issue is closed

Both workflows call the Node scripts rather than generating markdown inline in bash. The previous inline version produced invalid YAML (the generated --- front matter terminated the workflow's block scalar), which is why the automation never ran.

Running it manually​

Release post β€” Actions β†’ Sync App Releases to Blog β†’ Run workflow:

  • repo_name e.g. HRM-and-Payroll
  • release_tag e.g. 28.0.14
  • release_name, release_body optional
  • fallback_days optional (default 90)

Issue posts β€” Actions β†’ Sync Closed Issues to Blog β†’ Run workflow:

  • days_back β€” how far back to scan (default 30)

Locally:

cd doc

# release post
GITHUB_TOKEN=<pat> REPO_NAME=HRM-and-Payroll RELEASE_TAG=28.0.14 \
node scripts/sync-release-to-blog.js

# issue posts (default 30 days)
GITHUB_TOKEN=<pat> node scripts/sync-issues-to-blog.js
GITHUB_TOKEN=<pat> node scripts/sync-issues-to-blog.js 45
GITHUB_TOKEN=<pat> node scripts/sync-issues-to-blog.js --days=45

Blog structure​

blog/
β”œβ”€β”€ 2026-09-10-release-hrm-payroll-28-0-14.md ← release posts (root)
β”œβ”€β”€ updates/ ← per-issue posts
β”‚ └── 2026-08-31-issue-487-....md
└── authors.yml

Troubleshooting​

Blog post not created after a release?

  • Check DOCS_REPO_TOKEN exists in the app repo
  • Check notify-release.yml is present in the app repo
  • Check the Actions tab of npsbeograd.github.io for the dispatch run

Task list is empty?

  • The issue must be closed within the release window (between the previous and current release)
  • The issue needs an app label (repoName, shortCode or displayName)
  • Verify NPS_SUPPORT_TOKEN is set β€” without it the API returns 404 for the private repo

Want to edit a post?

  • Posts are plain markdown in doc/blog/; edit and commit.