# Release Day

> Orchestrate a full release-day workflow for iOS, Android, or Garmin apps: verify the main branch CI is green, generate a changelog from git history, draft platform-ready store listing copy, trigger semantic-release (or tag manually), wait for the build, and queue the App Store submission. Use when the user says "cut a release", "ship version X", "it's release day", or "prepare the release".

- Skill: `oleg-koval/release-day` (Agent Skill)
- Install (CLI): `npx skillmds@latest add oleg-koval/release-day`
- Raw SKILL.md: https://api.skillmd.com/api/skills/oleg-koval/release-day/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- License: MIT
- Author: oleg-koval (https://skillmd.com/u/oleg-koval)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/oleg-koval/release-day

---


> 🤖 *Auto-generated by **weekly-pattern-learner** · changelog-generator, store-listing-copy, and apple-store-submit each reference each other in their Chaining tables but no skill chains them into a single end-to-end release-day workflow*

# Release Day

Drive a release from "code is ready" to "build submitted" in one orchestrated pass.
Each phase validates its output before proceeding: the skill stops and reports
rather than pushing a broken release.

## Inputs

- **Platform** (required): `ios`, `android`, `garmin`, or `all`
- **Version** (optional): explicit version to release; if omitted, semantic-release
  determines the next version automatically
- **Branch** (optional, default: `main`): branch to release from
- **--dry-run** (optional): run all checks and generate all copy without pushing
  the tag or submitting to the store

## Phase 1: Verify the branch is ready

```bash
BRANCH="${Branch:-main}"
git fetch origin "$BRANCH"
BRANCH_SHA=$(git rev-parse "origin/$BRANCH")

# Validate optional Version input
if [ -n "$Version" ]; then
  if ! echo "$Version" | grep -qE '^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then
    echo "Error: Version '$Version' is not a valid SemVer (e.g. 1.2.3 or v1.2.3)" >&2
    exit 1
  fi
fi

git status --short

# Last release tag
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "none")

# Commits since last release
if [ "$LAST_TAG" = "none" ]; then
  git log "origin/$BRANCH" --oneline --no-merges | head -20
else
  git log "${LAST_TAG}..origin/$BRANCH" --oneline --no-merges | head -20
fi

# CI state on branch
gh run list --branch "$BRANCH" --limit 3 --json status,conclusion,workflowName \
  --jq '.[] | "\(.workflowName): \(.conclusion // .status)"'
```

**Stop if:**
- `git status --short` is non-empty: there are uncommitted changes
- The latest CI run on main is failing or still in progress
- There are zero commits since the last tag (nothing to release)

Report the problem clearly and stop. Do not proceed to a release if the branch
is not clean and green.

## Phase 2: Generate changelog

Run `olko:changelog-generator`.

Use the range `${LAST_TAG}..origin/${BRANCH}` (or `origin/${BRANCH}` if no prior tag exists). Produce a
structured changelog with sections: New Features, Improvements, Bug Fixes, Breaking
Changes. Strip internal commits (refactor, test, ci, chore without behavior impact).

Write the changelog to:
```bash
RELEASE_DIR="$(mktemp -d)/release-$(date +%Y-%m-%d)"
mkdir -p "$RELEASE_DIR"
# changelog-generator writes to CHANGELOG.md: validate before proceeding
if [ ! -s CHANGELOG.md ]; then
  echo "Error: CHANGELOG.md is missing or empty, cannot continue." >&2
  exit 1
fi
cp CHANGELOG.md "$RELEASE_DIR/changelog.md"
```

Review the generated changelog for accuracy before continuing. If commits are
ambiguous (e.g. `fix: thing`), use `git show <sha>` to read the diff and
re-classify the entry.

## Phase 3: Draft store listing copy

Run `olko:store-listing-copy` with `--platform <platform>`.

This reads the changelog from Phase 2 and drafts:
- iOS: Title, Subtitle, What's New (4000 chars), Keywords (100 chars)
- Android: Title (30), Short Description (80), Release Notes (500)
- Garmin: Description, What's New (1500 chars)

The skill validates field lengths and known store-validator rules (e.g., no `<`/`>` in
Garmin copy) before delivering. Output goes to:
```
store-copy-v<version>-<YYYY-MM-DD>.md
```

**Do not proceed until store copy passes validation.** Fix any overflow or forbidden
character before tagging.

## Phase 4: Trigger the release

### If semantic-release is configured

```bash
# Dry-run first to confirm version and release notes
set -o pipefail
if ! npm run release:dry-run 2>&1 | tail -40; then
  echo "Dry-run failed, aborting release." >&2
  exit 1
fi

# On confirmation (or with --dry-run: stop here)
git push origin "$BRANCH"   # trigger CI which runs semantic-release
```

Wait for the GitHub Actions release workflow to complete:

```bash
# Capture run ID for the push commit so we poll the right run
sleep 10
RUN_ID=$(gh run list --branch "$BRANCH" --workflow ci-release.yml --limit 1 \
  --json databaseId,headSha \
  --jq "[.[] | select(.headSha == \"$BRANCH_SHA\")] | .[0].databaseId")

# Poll until the targeted run is done (timeout: 15 min)
STATUS="pending"
for i in $(seq 1 30); do
  STATUS=$(gh run view "$RUN_ID" --json conclusion \
    --jq '.conclusion // "pending"' 2>/dev/null || echo "pending")
  [ "$STATUS" = "success" ] && break
  [ "$STATUS" = "failure" ] || [ "$STATUS" = "cancelled" ] || [ "$STATUS" = "timed_out" ] && \
    echo "Release workflow ended with status: $STATUS" && exit 1
  sleep 30
done
if [ "$STATUS" != "success" ]; then
  echo "Release workflow timed out after 15 minutes." >&2
  exit 1
fi
```

### If releasing manually (no semantic-release)

```bash
# Determine next version (follow SemVer: breaking → major, new feature → minor, fix → patch)
NEXT_VERSION="vX.Y.Z"  # fill in based on changelog analysis

# Tag and push
git tag -a "$NEXT_VERSION" -m "Release $NEXT_VERSION"
git push origin "$NEXT_VERSION"

# Create GitHub release
gh release create "$NEXT_VERSION" \
  --title "$NEXT_VERSION" \
  --notes-file "$RELEASE_DIR/changelog.md"
```

## Phase 5: Confirm build artifact

Download and validate the platform-specific build artifact. Poll every 2 minutes
(timeout: 20 min). Do not proceed to Phase 6 until the artifact is confirmed present.

```bash
ARTIFACT_FOUND=false
for i in $(seq 1 10); do
  gh run download "$RUN_ID" --dir "$RELEASE_DIR" 2>/dev/null || true

  case "$Platform" in
    ios)
      IPA=$(find "$RELEASE_DIR" -name "*.ipa" | head -1)
      [ -n "$IPA" ] && ARTIFACT_FOUND=true && echo "iOS artifact: $IPA" && break
      ;;
    android)
      APK=$(find "$RELEASE_DIR" \( -name "*.apk" -o -name "*.aab" \) | head -1)
      [ -n "$APK" ] && ARTIFACT_FOUND=true && echo "Android artifact: $APK" && break
      ;;
    garmin)
      PRG=$(find "$RELEASE_DIR" dist -name "*.prg" 2>/dev/null | head -1)
      [ -n "$PRG" ] && ARTIFACT_FOUND=true && echo "Garmin artifact: $PRG" && break
      ;;
    all)
      IPA=$(find "$RELEASE_DIR" -name "*.ipa" | head -1)
      APK=$(find "$RELEASE_DIR" \( -name "*.apk" -o -name "*.aab" \) | head -1)
      PRG=$(find "$RELEASE_DIR" dist -name "*.prg" 2>/dev/null | head -1)
      [ -n "$IPA" ] && [ -n "$APK" ] && [ -n "$PRG" ] && ARTIFACT_FOUND=true && break
      ;;
  esac

  echo "Attempt $i/10: artifact not yet available, waiting 2 minutes..."
  sleep 120
done

if [ "$ARTIFACT_FOUND" != "true" ]; then
  echo "Error: required build artifact not found after 20 minutes, aborting." >&2
  exit 1
fi
```

## Phase 6: Submit to the store

**Skip this phase with `--dry-run`**: stop after Phase 3 and print the store copy
for manual paste.

Run the platform-specific submission workflow with the artifact from Phase 5:

**iOS:** Run `olko:apple-store-submit`: handles `altool` / `xcrun notarytool`
submission, watches for Apple's processing email, and catches common rejection patterns.

**Android:** Upload the `.aab` (or `.apk`) to Google Play using `fastlane supply`:

```bash
fastlane supply \
  --aab "$APK" \
  --track internal \
  --package_name "$(cat android/app/build.gradle | grep applicationId | awk '{print $2}' | tr -d '"')"
```

Promote from internal → production after QA sign-off.

**Garmin:** Run `olko:garmin-watchface` store publishing workflow: uploads the `.prg`
and the listing copy from Phase 3 via the Connect IQ developer portal.

**all:** Run iOS → Android → Garmin in sequence; report each result independently.

## Final report

```
Release day complete: YYYY-MM-DD

  Version:      vX.Y.Z (was vA.B.C)
  Platform:     ios / android / garmin / all
  Changelog:    N features, M fixes, K improvements
  Store copy:   validated ✓ (all fields within limits)
  Release:      tagged + GitHub Release created ✓
  Build:        CI green, artifact downloaded ✓
  Submission:   queued for App Store / Google Play / Connect IQ review

  Next:
    - Check Apple / Google / Garmin review status in 24–72h
    - Monitor crash rates via Crashlytics / Firebase after rollout
    - Run /review-past-performance at end of day to capture release learnings
```

## Safety guardrails

- Never push a tag or trigger a release if Phase 1 fails
- Never submit a store build without a validated Phase 3 output
- Always `--dry-run` first on a new repo or a new platform
- Keep the store copy file in $RELEASE_DIR until the submission is confirmed live

## Chaining

| Before this skill | After this skill |
|---|---|
| `olko:pr-to-green`: merge the release PR | Monitor store review status |
| `olko:semantic-release-beta`: promote beta to stable | `olko:changelog-generator` for next iteration |
| Manual: "all features are in, ship it" | Post-release monitoring |

