name: Release on: push: tags: - 'v*' workflow_dispatch: inputs: tag: description: 'Release tag name (defaults to the pushed tag ref)' required: false permissions: contents: write id-token: write packages: write jobs: release: runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Checkout uses: actions/checkout@v6 with: # The notes step diffs git log between the previous tag and this # one; a shallow clone has no tags, so fetch the full history. fetch-depth: 0 - name: Setup pnpm uses: pnpm/action-setup@v5 with: version: 10.33.0 - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: 22 cache: pnpm registry-url: https://registry.npmjs.org - name: Install dependencies run: pnpm install --frozen-lockfile - name: Check git diff run: git diff --check - name: Typecheck run: pnpm typecheck - name: Test run: pnpm test - name: Build run: pnpm build - name: Verify publish bundle completeness run: node scripts/check-publish-bundle.mjs - name: Smoke test built CLI run: | node bin/dsh-lark-bot.mjs --version node dist/cli.js --version - name: Generate release notes from conventional commits run: | TAG="${TAG_INPUT:-${GITHUB_REF_NAME}}" PREV_TAG="$(git tag --sort=-version:refname | sed -n 2p || true)" if [ -n "$PREV_TAG" ]; then HIGHLIGHTS="release-highlights/${TAG}.md" if [ -f "$HIGHLIGHTS" ]; then node scripts/release-notes.mjs --from "$PREV_TAG" --to "$TAG" \ --highlights "$HIGHLIGHTS" > "$RUNNER_TEMP/release-notes.md" else node scripts/release-notes.mjs --from "$PREV_TAG" --to "$TAG" \ > "$RUNNER_TEMP/release-notes.md" fi echo "notes_file=$RUNNER_TEMP/release-notes.md" >> "$GITHUB_OUTPUT" fi env: TAG_INPUT: ${{ inputs.tag }} id: notes - name: Publish both npm packages and build release artifacts run: | VERSION="$(node -p "require('./package.json').version")" SKIP=1 for pkg in dsh-lark-bot dsh-feishu-bot; do PUBLISHED="$(npm view "$pkg@$VERSION" version 2>/dev/null || true)" if [ "$PUBLISHED" != "$VERSION" ]; then SKIP=0; fi done if [ "$SKIP" = "1" ]; then echo "::notice::$VERSION already published on npm — skipping publish, packing artifacts only" node scripts/publish-dual-packages.mjs --pack-dir release-artifacts --skip-publish else node scripts/publish-dual-packages.mjs --pack-dir release-artifacts fi env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Generate SHA-256 checksums for release artifacts run: node scripts/generate-release-checksums.mjs release-artifacts - name: Remove existing release for tag (idempotent re-release) run: | TAG="${TAG_INPUT:-${GITHUB_REF_NAME}}" # List-based lookup also finds draft releases (the tags/{tag} # endpoint 404s for drafts, and its non-2xx body must not be # captured as an id). if ID="$(gh api "repos/${GITHUB_REPOSITORY}/releases?per_page=100" \ --jq ".[] | select(.tag_name == \"${TAG}\") | .id" 2>/dev/null | head -1)"; then if [ -n "$ID" ]; then attempt=0 until gh api "repos/${GITHUB_REPOSITORY}/releases/${ID}" -X DELETE; do attempt=$((attempt + 1)) if [ "$attempt" -ge 6 ]; then echo "::error::failed to delete existing release ${ID} after 6 attempts" exit 1 fi echo "retrying delete of release ${ID} (attempt ${attempt})..." sleep 20 done echo "::notice::removed existing release ${ID} for ${TAG}" else echo "no existing release for ${TAG}" fi else echo "no existing release for ${TAG}" fi env: GH_TOKEN: ${{ github.token }} TAG_INPUT: ${{ inputs.tag }} - name: Publish both packages to GitHub Packages run: | npm_config_file="$RUNNER_TEMP/github-packages.npmrc" printf '@plutokeating:registry=https://npm.pkg.github.com/\n//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}\n' > "$npm_config_file" NPM_CONFIG_USERCONFIG="$npm_config_file" node scripts/publish-dual-packages.mjs --github env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_PACKAGE_SCOPE: plutokeating - name: Create GitHub Release uses: softprops/action-gh-release@v3 with: tag_name: ${{ inputs.tag || github.ref_name }} files: | release-artifacts/*.tgz release-artifacts/*.tgz.sha256 # Fall back to GitHub's auto notes when no previous tag exists # (first release), so the body is never empty. generate_release_notes: ${{ steps.notes.outputs.notes_file == '' }} body_path: ${{ steps.notes.outputs.notes_file }} # Explicitly mark the just-published version as Latest. GitHub # otherwise defaults to the newest *published* release, so # republishing an old tag (e.g. backfilling notes) would silently # steal the Latest badge from the newest version. make_latest: true