Release-to-Blog Automation
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):
| # | Trigger | When it fires | Preferred |
|---|---|---|---|
| 1 | workflow_run on " Publish To AppSource" | after AL-Go finishes publishing to AppSource successfully | β yes |
| 2 | release: published | as 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.workflowsmatches 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:
-
Label matches the app. Each app in
doc/apps-config.jsonhas an explicitlabelsarray listing the real NPS-Support labels that belong to it. Matching is normalised (lowercase, non-alphanumerics stripped), andrepoName/shortCode/displayNameact 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-Support App 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
labelsinapps-config.jsonβ nothing else needs to change. -
Closed inside the release window β between the previous release's
published_atand this release'spublished_at. When there is no previous release it falls back toFALLBACK_DAYS(default 90).
Setupβ
1. Secretsβ
| Secret | Where | Why |
|---|---|---|
DOCS_REPO_TOKEN | each app repo | PAT (repo scope) to dispatch to the docs repo |
NPS_SUPPORT_TOKEN | npsbeograd.github.io | PAT (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:
fetch-releases.jsβ refreshesdoc/src/data/releases.json, the build-time data behind the /releases page (every published release across the whole organisation).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 arepository_dispatchwas missed, the nightly run backfills it.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-upthat touchesdoc/ - after any sync workflow finishes successfully (bot commits made with
GITHUB_TOKENnever triggerpush, 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β
| File | Contents |
|---|---|
src/data/releases.json | light 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.json | hand-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β
| File | Purpose |
|---|---|
doc/scripts/fetch-releases.js | pulls all org releases β src/data/releases.json |
doc/scripts/sync-recent-releases.js | ensures recent releases have a blog post (backfill) |
doc/scripts/sync-release-to-blog.js | builds a single release post (single source of truth) |
doc/scripts/sync-issues-to-blog.js | builds per-issue update posts |
doc/src/pages/releases.js | the /releases changelog page |
.github/workflows/nightly-sync.yml | nightly: releases data + both kinds of posts |
.github/workflows/sync-release-to-blog.yml | receives the dispatch, runs the release script |
.github/workflows/sync-issues-to-blog.yml | runs 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_namee.g.HRM-and-Payrollrelease_tage.g.28.0.14release_name,release_bodyoptionalfallback_daysoptional (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_TOKENexists in the app repo - Check
notify-release.ymlis 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,shortCodeordisplayName) - Verify
NPS_SUPPORT_TOKENis 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.