When you release a course, promote it internally so the right teams and stakeholders know it is live and can share it with learners.
In this lesson, you will learn how to:
-
Promote a release to the right internal teams
-
Post course metadata to Asana automatically on merge
-
Send a Slack promo when a PR with the Release label is merged
-
Request SME feedback on Slack when the sme-review label is applied
-
Find and delete messages posted by the bot
Where to promote
Plan where to announce each release so the right people see it:
-
Asana – Create tasks on the right boards so marketing, content, and product can track and act. The repo can post course metadata to Asana automatically when a labelled PR is merged; the next section explains how to configure it.
-
Slack – Post in #graphacademy and any other channels that care about new or updated courses.
-
Team and SME channels – Notify subject-matter experts and relevant team channels so they can share the course with their audiences.
Writing blog posts for promotion
Include writing a blog post in your promotion plan. A short post on the Neo4j blog or developer blog helps learners discover the course and supports discoverability.
Posting to Asana
When a PR is merged with a label such as Release, Draft, or Enhancement, a GitHub Actions workflow can post course metadata to Asana so the right boards and assignees get a task.
How it works
The workflow:
-
Finds which
course.adocfiles were changed in the PR -
Derives the course slug(s) (e.g.
aura-agents) -
Loads the matching config file from
config/promo/(e.g.release.jsonfor the Release label) -
For each destination in that config, creates an Asana task with the course title, caption, link, key points, and categories, and assigns it to the right board, section, and assignee
Config files
Destination boards, assignees, and Slack channels are defined in combined JSON config files in config/promo/. Each label type can have its own config:
-
release.json– used when the Release label is merged -
draft.json– for a Draft label (create one if needed) -
enhancement.json– for an Enhancement label (create one if needed)
The workflow chooses the file from the merged PR’s label and the ASANA_CONFIG / SLACK_CONFIG variables (default release). The same file contains asana.destinations and slack.channels.
Each entry in asana.destinations has:
-
project– Asana project (board) GID -
section– optional section (column) GID -
assignee– optional user GID -
name– optional label for that destination (e.g. "GraphAcademy Marketing")
Example (config/promo/release.json):
{
"asana": {
"destinations": [
{
"name": "GraphAcademy Marketing",
"project": "1208974749704120",
"section": "1208974749704126",
"assignee": "1202055636798551"
}
]
},
"slack": {
"channels": ["graphacademy"]
}
}Tasks are created with a due date two weeks from the day they are posted.
Finding GIDs
You need three GIDs per destination: project (board), section (column), and assignee (person). Set ASANA_API_KEY in .env (or the environment) before running any of the commands below.
Easiest: build a destination from the board URL
If you have the Asana board URL and know the section name and assignee name, use the combined command. It looks up all GIDs and prints the destination JSON ready to paste into config/promo/release.json (under asana.destinations):
npm run asana:url-to-destination -- \
--url "https://app.asana.com/1/200109678723468/project/1211853693432549/list/1211854278859853" \
--section "Marquee Assets in Production" \
--assignee "Greg Posten"-
--url– the full Asana project/list URL (the project GID is read from it) -
--section– the exact or partial section name (e.g. column name) -
--assignee– the exact or partial person name (or email) -
--name– optional label for this destination in the config
The script prints the project name and GID, the section and assignee (or lists options if a name does not match), then the destination object. Copy the JSON into the destinations array in your config file.
Project GID from the URL
The project GID is the number that appears after /project/ in the board URL.
Example: in https://app.asana.com/1/200109678723468/project/1211853693432549/list/…; the project GID is 1211853693432549.
The /list/ number in the URL is not the section GID; use the commands below to get the real section GID.
List sections for a board
To get the section GID for a column (e.g. "Marketing & Outreach", "Marquee Assets in Production"), list sections for the project:
npm run asana:list-sections -- <project_gid>The output shows each section name and its GID. Use the GID in the section field of your destination.
List users (assignees)
To get a user GID for the assignee field, list users in the project’s workspace. You can filter by name or email:
npm run asana:list-assignees -- --project <project_gid>
npm run asana:list-assignees -- --project <project_gid> --search "Greg"Use the GID of the right user as assignee in your destination.
Running locally
To post metadata for a course to Asana without merging a PR, run:
ASANA_CONFIG=release npm run release:asana -- <course_slug>Example:
ASANA_CONFIG=release npm run release:asana -- aura-agentsUse ASANA_CONFIG to choose the config file (e.g. release, draft, config.example). Ensure ASANA_API_KEY is set (e.g. in .env).
Workflow summary
-
Trigger: PR merged with a label (e.g. Release)
-
Config:
config/promo/{ASANA_CONFIG}.json(e.g.release.json), sectionasana.destinations -
Metadata: Read from each merged course’s
course.adoc(title, caption, key points, categories); link is built from the course slug -
Output: One Asana task per destination, with due date in two weeks
Posting to Slack
The same Release workflow posts a promo message to Slack. The message is built from an AsciiDoc template in which course attributes are replaced, then sent to each channel in the config.
Template and config
-
Template –
asciidoc/shared/release/{SLACK_CONFIG}.adoc(e.g.release.adoc). The title is merged from the course document’sgetTitle()(available in the template asInternal promotion); other attributes are{caption},{link},{key-points},{categories}. The template is rendered by the same AsciiDoc processor used elsewhere in the repo; the output is then converted to Slack-friendly text. -
Config – The same file
config/promo/release.jsonhas aslack.channelsarray: channel names (e.g.graphacademy) or channel IDs. The workflow usesSLACK_CONFIG=releaseand the secretSLACK_BOT_TOKEN(Bot User OAuth Token withchat:write).
Running locally
To post the Slack promo for a course without merging a PR:
SLACK_CONFIG=release npm run release:slack -- <course_slug>Ensure SLACK_BOT_TOKEN is set (e.g. in .env). The same template and config are used as in the workflow.
Managing Slack messages
If you need to find or remove a message the bot posted, two commands are available. Both require SLACK_BOT_TOKEN set in .env.
Pass the channel ID, not the channel name. Find it in Slack by right-clicking the channel and selecting View channel details — the ID appears at the bottom of that dialog.
Listing bot messages
List the most recent messages the bot posted in a channel:
npm run slack:list-messages -- --channel <channel_ID>Use --limit to fetch more messages, and --search to filter by keyword:
npm run slack:list-messages -- --channel <channel_ID> --limit 20 --search "aura-agents"The output shows a Timestamp column. Copy that value to delete a specific message.
Deleting a bot message
npm run slack:delete-message -- --channel <channel_ID> --ts <timestamp>Use --dry-run to confirm what would be deleted before committing:
npm run slack:delete-message -- --channel <channel_ID> --ts <timestamp> --dry-runRequesting SME review
Before merging a PR, you can request subject-matter expert feedback by applying the sme-review label to the PR. A GitHub Actions workflow detects the label and posts a review request to the #graphacademy Slack channel.
The message includes the course title, caption, and a link to the course on GraphAcademy, and asks reviewers to share corrections or suggestions in thread.
How it works
-
Applying the sme-review label triggers the
sme-review-slack.ymlworkflow -
The workflow finds which
course.adocfiles are in the PR and derives the course slug(s) -
It posts using the template
asciidoc/shared/release/sme-review.adocand sends the message to the channels defined inconfig/promo/sme-review.json
Edit asciidoc/shared/release/sme-review.adoc to change the message wording, and edit config/promo/sme-review.json to add or remove channels.
Running locally
To send an SME review request for a course without applying a label to a PR:
npm run review:slack -- <course_slug>Example:
npm run review:slack -- aura-agentsEnsure SLACK_BOT_TOKEN is set (e.g. in .env).
Summary
In this lesson, you learned how to promote a release internally and request pre-release feedback using GitHub PR labels.
-
Asana – Merging a PR with the Release label posts course metadata to the boards and assignees configured in
config/promo/release.json(asana.destinations). Useasana:url-to-destination,asana:list-sections, andasana:list-assigneesto build destination entries. -
Slack release promo – The same merge posts to the channels in
config/promo/release.json(slack.channels) using the template atasciidoc/shared/release/release.adoc. Runrelease:slack <slug>locally to test. -
SME review request – Applying the sme-review label to an open PR posts a feedback request to the channels in
config/promo/sme-review.jsonusing the template atasciidoc/shared/release/sme-review.adoc. Runreview:slack <slug>locally to test. -
Managing bot messages – Use
slack:list-messages --channel <ID>to find recent bot posts andslack:delete-message --channel <ID> --ts <timestamp>to remove one. Pass the channel ID (not the name); find it in Slack by right-clicking the channel and selecting View channel details.