universal-emoji-parser
Version:
This tool allow parse unicode and emoji codes to html images using emojilib && Twemoji CDN
403 lines (284 loc) • 14.3 kB
Markdown
---
name: npm-publish-walkthrough
description: Full release flow including GitHub Actions internals, manual fallback, and rollback strategies
---
# Skill: `npm-publish-walkthrough`
Comprehensive reference for releasing Universal Emoji Parser to npm. Covers the automated CI flow, the manual fallback, and what to do when things go wrong.
For the day-to-day procedural commands, see [`/release-npm`](../commands/release-npm.md). This skill explains the full picture.
## The release model
Universal Emoji Parser uses a **merge-to-main = release** model:
1. Every PR merge to `main` triggers `release_and_publish.yml`
2. The workflow patch-bumps the version, builds, tags, creates a GitHub Release, and publishes to npm
3. Humans don't run `npm version` or `npm publish` manually under normal conditions
Tradeoffs:
- ✅ Every change ships immediately — no batching, no delayed release
- ✅ Release notes are automatic
- ✅ Patch number reflects merge count
- ❌ Can't easily ship a major version (the workflow auto-runs `npm version patch`)
- ❌ Unreviewable releases — by the time you see version X, it's already on npm
## The CI workflow in detail
`.github/workflows/release_and_publish.yml`. Trigger:
```yaml
on:
pull_request:
branches: [main]
types: [closed]
```
…with `if: github.event.pull_request.merged == true` on every job (so closing-without-merging doesn't release).
### Jobs in order
#### 1. `check_pr_size_label`
Reads PR labels, looks for `Size - XS / S / M / L / XL / XXL` (set earlier by `pull_request_check.yml`), emits a colored emoji indicator. Pure metadata for the channel notification.
#### 2. `notify_on_channel_start`
Posts "deployment started" to a DailyBot Slack-like channel via:
```
POST https://api.dailybot.com/v1/send-message/
Headers: X-API-KEY: ${{ secrets.DAILYBOT_API_KEY }}
```
Includes PR number, title, body, size label, workflow URL.
#### 3. `deploy_setup`
```yaml
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '24' }
- uses: actions/cache@v4
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-build-cache-node-modules-${{ hashFiles('**/package-lock.json') }}
- if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }}
run: npm install
```
Cache key uses `package-lock.json`'s hash — but we don't commit `package-lock.json`. So the hash is empty/missing, and the cache key collapses to `${{ runner.os }}-build-cache-node-modules-`. This effectively means "always cache hits across all runs that share the OS." Good for speed; bad if a security advisory requires invalidating.
To force a fresh install, bump the cache key (e.g., add `-v2`).
#### 4. `deploy_validate_linters_and_code_format`
```yaml
- run: npm run eslint:check
- run: npm run prettier:check
```
Hard gate. Both must pass.
#### 5. `deploy_tests`
```yaml
- run: npm run test
```
All Mocha specs must pass. The regenerator test (`prepareEmojiLibJson.test.ts`) is `it.skip`'d so it doesn't run.
#### 6. `build`
```yaml
- run: |
npm run build
if [ ! -d "dist" ]; then
echo "⚠️ Error: dist folder does not exist."
exit 1
fi
```
Webpack production build. The output (`dist/`) is cached for the publish job.
> **Gotcha**: this step doesn't run `npm run build:tsc`. Without it, `dist/index.d.ts` may be missing from the published tarball. If consumers report "no types," the fix is to add `npm run build:tsc` here.
#### 7. `release_and_publish`
The actual release. Steps:
```yaml
- uses: actions/checkout@v4
with:
fetch-depth: '30' # need history for release notes
token: ${{ secrets.AUTOMATION_GITHUB_TOKEN }}
- uses: actions/setup-node@v4
with:
node-version: '24'
registry-url: https://registry.npmjs.org/
- run: |
git config user.name "🤖 DailyBot"
git config user.email "ops@dailybot.com"
- run: |
bash .github/scripts/get_github_release_log.sh
if [[ ! -f git_logs_output.txt ]]; then
echo "⚠️ No description found for release body content."
exit 1
fi
- run: |
npm run release # npm version patch -m "[🤖 DailyBot] New release to v%s launched 🚀"
git push --follow-tags origin main
- run: |
GITHUB_RELEASE_TAG=$(git describe --tags $(git rev-list --tags --max-count=1))
if [[ -z $GITHUB_RELEASE_TAG ]]; then
echo "⚠️ No release tag found."
exit 1
fi
echo "::set-env name=GITHUB_RELEASE_TAG::$GITHUB_RELEASE_TAG"
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
- uses: ncipollo/release-action@v1
with:
name: Release ${{ env.GITHUB_RELEASE_TAG }}
tag: ${{ env.GITHUB_RELEASE_TAG }}
bodyFile: git_logs_output.txt
token: ${{ secrets.AUTOMATION_GITHUB_TOKEN }}
- run: |
if [ ! -d "dist" ]; then
echo "⚠️ Error: dist folder does not exist."
exit 1
fi
npm publish
echo "package_version=$(npm pkg get version)" >> $GITHUB_OUTPUT
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- run: |
PR_MERGED=$(jq --raw-output .pull_request.merged "$GITHUB_EVENT_PATH")
if [ "$PR_MERGED" = "true" ]; then
git push origin --delete "${{ github.event.pull_request.head.ref }}"
fi
```
Note the `ACTIONS_ALLOW_UNSECURE_COMMANDS: true` and `::set-env`. These are deprecated GitHub Actions syntax (set-env was disabled by default in 2020). The workflow predates the modern alternative (`echo "TAG=..." >> $GITHUB_ENV`). It still works because of the explicit `ACTIONS_ALLOW_UNSECURE_COMMANDS` toggle, but it's a tech-debt item — eventually GitHub may remove it. When that happens, fix the syntax:
```bash
echo "GITHUB_RELEASE_TAG=$GITHUB_RELEASE_TAG" >> $GITHUB_ENV
```
#### 8. `cleanup_caches`
Triggers `cleanup_caches.yml` via `repository_dispatch` to GC stale GHA caches.
#### 9. `notify_on_channel_end`
`if: always()` — runs even if earlier jobs failed. Posts the per-job status (✅/❌/⏩/❓) plus overall success/failure to the DailyBot channel. On failure, includes `vars.USERS_TO_NOTIFY` (a Slack mention list).
## The release notes script
`.github/scripts/get_github_release_log.sh`:
```bash
git log --pretty=oneline | sed 's/[^ ]* *//' > git_logs.txt
while read text_line; do
if [[ "$text_line" =~ "[🤖 DailyBot] New release to v" ]]; then
break
fi
if [[ ! "$text_line" =~ "Merge branch 'main'" ]] && [[ ! "$text_line" =~ "Merge pull request" ]]; then
echo "🚩 $text_line" >> git_logs_output.txt
fi
done < git_logs.txt
```
Walks `git log` from HEAD until the previous `[🤖 DailyBot] New release to v` commit, collects everything in between (skipping merge commits), prefixes each with `🚩`. The result is the body of the GitHub Release.
Implications:
- **The bot's commit message format is the boundary marker** — if you change it, you must also change the regex in this script
- **Release notes are commit-message-quality** — write good commit messages; they become the changelog
- **Merges are filtered** — if your repo's merge style is "Squash and merge," the squash commit's message is what's recorded (good); if "Create a merge commit," the merge message is filtered (so the underlying commits are preserved as long as they're not squashed away)
## The `npm version` step
```bash
npm version patch -m "[🤖 DailyBot] New release to v%s launched 🚀"
```
What this does:
1. Reads current version from `package.json`
2. Increments the patch number (e.g., `2.0.79` → `2.0.80`)
3. Writes the new version back to `package.json`
4. Creates a git commit: `[🤖 DailyBot] New release to v2.0.80 launched 🚀`
5. Creates a git tag: `v2.0.80`
If `npm version` fails (working tree dirty, network issue), the workflow aborts and no release happens.
### Why patch only
`npm run release` is hardcoded to patch:
```json
"release": "npm version patch -m \"[🤖 DailyBot] New release to v%s launched 🚀\""
```
For minor or major releases, you have two options:
**Option A: Manually pre-bump in the PR**
In the PR that triggers a minor release:
```bash
# In your PR branch, before final review
$EDITOR package.json # change version from 2.0.79 to 2.1.0
git commit -am "chore: bump version to 2.1.0 for release"
git push
# Merge the PR
# Workflow runs `npm version patch`, bumps 2.1.0 → 2.1.1
# Final published version is 2.1.1, not 2.1.0
```
That last detail (`2.1.1`, not `2.1.0`) means the "minor" version users see is the `.1` patch — minor enough not to matter for semver, awkward for release notes.
**Option B: Disable auto-bump for the release**
Edit the workflow temporarily to skip `npm run release`, run `npm version minor` locally, push the tag, re-enable the workflow. More involved.
Neither is great. A future improvement: add a `[skip auto-bump]` PR-title convention or a `release-type: minor` PR label that the workflow honors.
## Manual release procedure
Required when: CI is down, the workflow is broken, an emergency hotfix needs to ship in minutes.
See [`/release-npm`](../commands/release-npm.md) for the procedural walkthrough. Key points:
1. **Always run the full check sequence** — never publish unverified
2. **Use `npm version`** to bump + commit + tag atomically (don't edit `package.json` by hand)
3. **`git push --follow-tags`** — pushing without `--follow-tags` leaves the tag local
4. **`npm publish`** requires `npm login` or `NODE_AUTH_TOKEN` env var
5. **Smoke-test in a fresh directory** after publish — `npm install` the published version and verify
## Rollback strategies
### Within 72 hours of publish, with no downloads
```bash
npm unpublish universal-emoji-parser@<bad-version> --force
```
This is the only way to make the bad version disappear from npm. Limited window.
### After 72 hours
You can't unpublish. Two options:
**Deprecate**:
```bash
npm deprecate universal-emoji-parser@<bad-version> "Broken; use <good-version>"
```
Adds a console warning when consumers install the deprecated version. Doesn't block install.
**Publish a fix**:
Bump again (usually patch) with the fix and ship. Consumers `npm update` to get the fix.
For severe issues (security, malware in a dep), npm support can intervene faster — open a ticket.
## Common failure modes
### `npm publish` 401 Unauthorized
- `secrets.NPM_TOKEN` is expired
- Token is account-wide but doesn't have the new package's name in its allowlist
- Account 2FA is `auth-and-writes` mode and the automation token can't satisfy it (use an automation-type token specifically — npm distinguishes)
Fix: regenerate the token in npm settings → Profile → Access Tokens → Generate New Token → "Automation". Update `secrets.NPM_TOKEN` in GitHub repo settings.
### `npm version` says "Git working directory not clean"
Some prior step modified the repo. Common causes:
- A test wrote to a file in the repo (regenerator, sloppy test)
- `dist/` was modified between cache restore and `npm version` step
- `package-lock.json` was generated by `npm install` and isn't in `.gitignore`
Fix: add the offending file to `.gitignore`, or `git checkout -- .` before `npm version`.
### `git push --follow-tags` rejected
Branch protection on `main` requires PRs, blocking direct pushes. The workflow needs a way through:
- Use a fine-grained PAT or GitHub App token with **bypass branch protection** privileges
- Or weaken branch protection for the bot user
- Or change the workflow to not push directly (push to a release branch, then merge — much more complex)
### `actions/cache` cache miss every run
If the cache key uses `hashFiles('**/package-lock.json')` but you don't commit the lockfile, the hash is empty → cache key is constant → caches collide. This usually still works (collisions are valid hits), but if you ever need to force-refresh, bump the cache key suffix:
```yaml
key: ${{ runner.os }}-build-cache-node-modules-v2-${{ hashFiles('**/package-lock.json') }}
```
## What gets published
`.npmignore` controls the tarball:
```
Excluded:
.vscode_example, .vscode, .devcontainer_example, .devcontainer
src, test
.babelrc, .env, .env_example
.gitignore, .travis.yml
package-lock.json
tsconfig.json, webpack.config.js
docker, .github
eslint.config.mjs (flat ESLint; `.eslintignore` removed — use `ignores` in config)
get_github_release_log.sh
git_logs.txt, git_logs_output.txt
.editorconfig, .prettierrc
Included by default (everything not excluded):
dist/index.js
dist/index.d.ts
dist/lib/type.d.ts
dist/*.map
package.json
README.md
LICENSE
```
Verify before publishing:
```bash
npm pack --dry-run
```
If the output includes `src/`, `test/`, or config files, fix `.npmignore`.
## Side effect: branch deletion
The final step of `release_and_publish.yml`:
```bash
git push origin --delete "${{ github.event.pull_request.head.ref }}"
```
Deletes the source branch of the merged PR. This is convenient (keeps the branch list clean) but **irreversible** — you can't recover the branch from GitHub UI after this.
If a developer wants to keep their branch (e.g., for follow-up PRs), they have to either:
- Re-push the branch after the release runs
- Make a backup tag before merging
This isn't documented prominently anywhere — be prepared for the question.
## Notifications
The workflow posts to a DailyBot Slack-like channel using:
- `secrets.DAILYBOT_API_KEY` — auth header
- `vars.DAILYBOT_DEPLOYMENT_NOTIFICATION_CHANNEL` — channel ID
- `vars.DAILYBOT_WORKFLOWS_NOTIFICATION_CHANNEL` — channel ID for `check_packages_versions.yml`
- `vars.USERS_TO_NOTIFY` — Slack mention list for failures
If you fork and don't use DailyBot, either:
1. Replace the curl calls with your own webhook (Slack, Discord, Teams)
2. Strip the `notify_on_channel_*` jobs and dependencies entirely
Without the secrets/vars, the curl calls fail silently — the rest of the pipeline still works, but you lose visibility.
## Why this is over-engineered for a small library
Honest answer: it isn't, for DailyBot's organizational needs. The size labels, the channel notifications, the auto-merge of dep PRs — these reflect a pattern DailyBot uses across many small libraries. Forking this for a different organization will likely simplify.
For a fork, see [Fork Customization → Step 5](../../docs/FORK_CUSTOMIZATION.md#step-5--ci-release-workflows).