# Post Daily New Issues

> Post the daily digest of new Sentry issues for the current stable release to an on-call Slack channel. Use when the user asks to run or post the daily stable Sentry digest for on-call.

- Skill: `warpdotdev/post-daily-new-issues` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add warpdotdev/post-daily-new-issues`
- Raw SKILL.md: https://api.skillmd.com/api/skills/warpdotdev/post-daily-new-issues/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: warpdotdev (https://skillmd.com/u/warpdotdev)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/warpdotdev/post-daily-new-issues

---


# post-daily-new-issues

Post a daily digest of *new* Sentry issues for the current release on **`$DIGEST_CHANNEL`** (default `stable`) to an on-call Slack channel, tagging the on-call usergroup. The digest is **scoped to a single calendar date** so a scheduled agent can run this once per weekday without re-reporting issues that were already shared on a previous day.

## Configuration

This skill reads environment-specific values from environment variables (see the repo `README.md`):

- `SLACK_BOT_TOKEN` — Slack bot token with `channels:read`, `chat:write`, `usergroups:read` scopes.
- `SENTRY_AUTH_TOKEN` — Sentry auth token with `event:read` and `project:read` scopes. Used to call the Sentry REST API directly.
- `SENTRY_ORG` — Sentry org slug.
- `SENTRY_PROJECT` — Sentry project slug (e.g. your release client project).
- `SENTRY_PROJECT_ID` — numeric Sentry project ID (used to build release URLs).
- `RELEASE_SLACK_CHANNEL` — Slack channel name (or ID) to post the digest in.
- `ONCALL_SLACK_GROUP` — Slack usergroup handle for the on-call (e.g. `oncall-primary`).
- `RELEASE_VERSIONS_URL` — optional public URL returning a JSON document with the current release version. Omit if you pass a version explicitly.
- `DIGEST_CHANNEL` — channel whose release the digest reports on (default `stable`); also selects the version key read from `RELEASE_VERSIONS_URL`.

## Step 0: Decide whether to run today (weekend skip)

This skill is intentionally a no-op on Saturday and Sunday. Compute the day of week in UTC:

```bash
DOW=$(date -u +%u)   # 1=Mon ... 7=Sun
```

- If `DOW` is `6` (Sat) or `7` (Sun): print a short note (e.g. `"Skipping: weekend"`) and exit without posting to Slack.
- Otherwise continue.

If the caller passed an explicit target date (see Step 2), skip this step entirely and use what they provided.

## Step 1: Find the current release version

If `RELEASE_VERSIONS_URL` is set, read the current version for `$DIGEST_CHANNEL` from it. The exact JSON path depends on your release metadata format; the channel key is selected by `$DIGEST_CHANNEL`, for example:

```bash
RELEASE_VERSION=$(curl -fsS "$RELEASE_VERSIONS_URL" | jq -r --arg ch "$DIGEST_CHANNEL" '.[$ch].version')
```

`RELEASE_VERSION` looks like `v0.YYYY.MM.DD.HH.MM.stable_NN` (a documented example template — adjust to match the version scheme your release pipeline emits). If `RELEASE_VERSIONS_URL` is unset, ask the user for the current release version. If the field is missing or `curl` fails, stop and surface the error — do not post a partial digest.

## Step 2: Compute the target date window

The window is a half-open UTC interval `[START_DATE, END_DATE)` (i.e. `firstSeen >= START_DATE AND firstSeen < END_DATE`). Both are `YYYY-MM-DD` strings.

The default behavior depends on which weekday the agent is running:

- **Tue–Fri** (`DOW` 2–5): report yesterday only.
  - `START_DATE` = yesterday (UTC), `END_DATE` = today (UTC).
- **Mon** (`DOW` 1): report Saturday + Sunday + the previous Monday's overnight, because the agent did not run on the weekend.
  - `START_DATE` = 3 days ago (UTC, that Saturday), `END_DATE` = today (UTC, that Monday).

On macOS (BSD `date`):

```bash
TODAY=$(date -u +%Y-%m-%d)
if [ "$DOW" = "1" ]; then
  START_DATE=$(date -u -v-3d +%Y-%m-%d)   # Saturday
else
  START_DATE=$(date -u -v-1d +%Y-%m-%d)   # Yesterday
fi
END_DATE=$TODAY
```

On Linux (GNU `date`, e.g. inside a remote environment):

```bash
TODAY=$(date -u +%Y-%m-%d)
if [ "$DOW" = "1" ]; then
  START_DATE=$(date -u -d '3 days ago' +%Y-%m-%d)
else
  START_DATE=$(date -u -d 'yesterday' +%Y-%m-%d)
fi
END_DATE=$TODAY
```

**Override**: if the user explicitly asks for a different window (e.g. "post the digest for 2026-04-25"), use their dates instead and skip the weekend check.

## Step 3: Fetch new issues from Sentry

Call the Sentry REST API directly with `$SENTRY_AUTH_TOKEN`. The query string must include both:

1. `firstRelease:<RELEASE_VERSION>` — restricts to issues whose first-seen release matches the live release build (this matches Sentry's "New Issues" tab on the release page).
2. `firstSeen:>=START_DATE firstSeen:<END_DATE` — narrows to the target window so already-reported issues from previous days are excluded. This is the **noise-control** mechanism; do not omit it.

Sort by frequency (`sort=freq`) and request `limit=100` so the true total and the top 3 are both stable even if there's a long tail of one-event issues.

```bash
QUERY="firstRelease:${RELEASE_VERSION} firstSeen:>=${START_DATE} firstSeen:<${END_DATE}"

ISSUES_JSON=$(curl -fsS -G "https://sentry.io/api/0/projects/${SENTRY_ORG}/${SENTRY_PROJECT}/issues/" \
  -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \
  --data-urlencode "query=$QUERY" \
  --data-urlencode "sort=freq" \
  --data-urlencode "limit=100")
```

The response is a JSON array. Each element has at least: `shortId`, `title`, `count` (event count, as a string), `userCount`, `firstSeen`, `lastSeen`, `permalink`. Parse it with `jq`.

**If the array is empty, exit silently — do NOT post anything to Slack.** Print a short note like `"No new issues in window; skipping post"` so the run log makes the no-op visible, then stop. The on-call channel should only get a message when there's something to act on; an empty digest just adds noise.

## Step 4: Compute the total and pick the top 3

The message has two distinct numbers, and they must come from different slices of the response:

- **`TOTAL_NEW_ISSUES`** — the full count of new issues in the window (used in the "N new issues since …" summary line). Compute this from the entire array, **before** any truncation.
- **Top 3 issues** — the first 3 entries by event count, used to populate the bullet list. The list is intentionally short to keep the digest scannable; the on-call clicks through to Sentry for the long tail.

For each of the top 3, capture:

- Short ID from `.shortId`
- Title from `.title` — truncate to ~140 chars on a single line
- Event count from `.count` (cast to int)
- User count from `.userCount`
- Issue URL: prefer `.permalink` if present, else build it as `https://${SENTRY_ORG}.sentry.io/issues/<SHORT_ID>`

If fewer than 3 issues exist, include all of them.

Example `jq` to compute both numbers in one pass:

```bash
TOTAL_NEW_ISSUES=$(printf '%s' "$ISSUES_JSON" | jq 'length')

BULLETS=$(printf '%s' "$ISSUES_JSON" | jq -r --arg org "$SENTRY_ORG" '
  [.[] | {
    short: .shortId,
    title: (.title | gsub("\n"; " ") | .[0:140]),
    events: (.count | tonumber),
    users: .userCount,
    url: (.permalink // ("https://" + $org + ".sentry.io/issues/" + .shortId))
  }]
  | sort_by(-.events)
  | .[0:3]
  | map("• <\(.url)|\(.short)> — \(.events) events, \(.users) users — \(.title)")
  | join("\n")
')
```

## Step 5: Resolve Slack IDs

Resolve once per run. If the channel ID and subteam ID are stable in your environment you may cache them, but always validate they still match.

```bash
# Channel ID for $RELEASE_SLACK_CHANNEL
CHANNEL_ID=$(curl -s -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
  "https://slack.com/api/conversations.list?types=public_channel,private_channel&limit=1000" | \
  jq -r --arg name "$RELEASE_SLACK_CHANNEL" '.channels[] | select(.name==$name) | .id')

# Subteam ID for the on-call usergroup
SUBTEAM_ID=$(curl -s -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
  "https://slack.com/api/usergroups.list" | \
  jq -r --arg handle "$ONCALL_SLACK_GROUP" '.usergroups[] | select(.handle==$handle) | .id')
```

If either lookup is empty, surface the error and stop — do not guess IDs.

## Step 6: Format and post the message

Use Slack Block Kit. The mention must be `<!subteam^${SUBTEAM_ID}>` so it actually pings the on-call group. Build a `text` fallback for notifications and search.

Build the Sentry release link first — every message must include it so the on-call can jump straight into the filtered "new issues" view:

```bash
RELEASE_URL="https://${SENTRY_ORG}.sentry.io/explore/releases/${RELEASE_VERSION}/?issuesType=new&project=${SENTRY_PROJECT_ID}"
```

The `issuesType=new` query param is what pre-selects the **New Issues** tab; do not omit it. `project=${SENTRY_PROJECT_ID}` is the numeric ID for your Sentry project.

Message structure:

1. **Header**: `<RELEASE_VERSION>: new Sentry issues for <date or range>`
2. **Section** with the subteam mention and a one-line summary that uses the **true total**, not the bullet count. Example:
   `<!subteam^S012345> — ${TOTAL_NEW_ISSUES} new issues since <START_DATE> · <RELEASE_URL|View in Sentry>`
   On a Monday run, phrase the window as `since Saturday` instead of a bare date.
3. **Section** listing only the **top 3** issues by event count, one per line, in this format:

```
• <ISSUE_URL|SHORT_ID> — <events> events, <users> users — <title>
```

4. **Section** with a triage reminder, e.g.:
   `_Please <${RELEASE_URL}|review the full New Issues tab in Sentry> and triage._`
   This is what tells the on-call the bullets are only a sample and that they need to click through.

Use Slack mrkdwn link syntax (`<URL|text>`) inside `mrkdwn` blocks. Keep titles on a single line; truncate with `…` if needed.

If `TOTAL_NEW_ISSUES` is zero, do not reach this step — the skill exits in Step 3 without posting. There is no "all clear" message; silence is the all-clear signal.

Date display:
- Tue–Fri: show `START_DATE` (e.g. `2026-04-28 (UTC)`).
- Mon: show the range, e.g. `Sat 2026-04-25 – Mon 2026-04-27 (UTC)`.

Post as a **top-level** message (no `thread_ts`). Suppress link previews so the message stays compact — with several Sentry links per post, unfurls would dominate the channel:

```bash
curl -s -X POST \
  -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
  -H "Content-Type: application/json" \
  --data "$(jq -n \
    --arg channel "$CHANNEL_ID" \
    --arg text "$TEXT_FALLBACK" \
    --argjson blocks "$BLOCKS_JSON" \
    '{channel: $channel, text: $text, blocks: $blocks, unfurl_links: false, unfurl_media: false}')" \
  "https://slack.com/api/chat.postMessage" | jq '{ok, ts, error}'
```

`unfurl_links: false` blocks regular URL previews; `unfurl_media: false` blocks image/video previews. Both are required — omitting either lets Slack render whichever class of preview it didn't see suppressed.

If `ok` is false, log the `error` field and stop. Common failures: `not_in_channel` (invite the bot), `missing_scope`, `channel_not_found`.

## Step 7: Confirm

Fetch and print the permalink so a human (or downstream agent) can click into the post:

```bash
curl -s -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
  "https://slack.com/api/chat.getPermalink?channel=$CHANNEL_ID&message_ts=<MESSAGE_TS>" | \
  jq -r '.permalink'
```

## Notes

- **Why scope by `firstSeen` and not just `firstRelease`?** Without the date filter, every daily run would re-post the same long-lived issues from earlier in the release's lifetime. Filtering by `firstSeen` on the target date is what makes the digest actionable and not noisy.
- **Why bundle the weekend on Monday instead of running on Sat/Sun?** If the on-call rotation is staffed Mon–Fri, weekend posts would go unread. Bundling on Monday surfaces anything that broke over the weekend in a single message.
- **Manual reruns**: if the user provides explicit `START_DATE` / `END_DATE` (e.g. for a backfill or to retry a missed day), use those and skip the weekday/weekend logic.
- A reusable helper that fetches and normalizes issues is provided at `scripts/fetch_new_issues.sh`.

