diff --git a/.github/workflows/dev-release.yml b/.github/workflows/dev-release.yml index 1c12c76e..e048677e 100644 --- a/.github/workflows/dev-release.yml +++ b/.github/workflows/dev-release.yml @@ -11,8 +11,7 @@ concurrency: jobs: release: - # Skip if commit message contains [skip ci] - if: "!contains(github.event.head_commit.message, '[skip ci]')" + if: ${{ github.ref == 'refs/heads/dev' && (github.event_name != 'push' || !contains(github.event.head_commit.message, '[skip ci]')) }} runs-on: ubuntu-latest permissions: @@ -26,6 +25,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + ref: dev # Always use the built-in workflow token; PAT rotation/breakage must not block dev releases. token: ${{ github.token }} @@ -65,12 +65,7 @@ jobs: # Use custom dev release script instead of semantic-release # This ensures dev versions follow {stable}-dev.{N} pattern chmod +x scripts/dev-release.sh - if ./scripts/dev-release.sh; then - echo "released=true" >> $GITHUB_OUTPUT - else - echo "Dev release failed or skipped" - echo "released=false" >> $GITHUB_OUTPUT - fi + ./scripts/dev-release.sh - name: Notify Discord if: success() && steps.release.outputs.released == 'true' @@ -87,28 +82,46 @@ jobs: if: success() && steps.release.outputs.released == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PREVIOUS_DEV_TAG: ${{ steps.release.outputs.previous_dev_tag }} + STABLE_TAG: ${{ steps.release.outputs.stable_tag }} run: | # Get version from package.json VERSION=$(jq -r '.version' package.json) # Get commits ONLY since last dev tag (not all release commits) # This prevents re-tagging issues from older releases - LAST_DEV_TAG=$(git tag -l "v*-dev.*" --sort=-v:refname | head -1 || echo "") - - if [ -n "$LAST_DEV_TAG" ]; then + if [[ -n "$PREVIOUS_DEV_TAG" ]]; then # Commits between last dev tag and current (excluding release commit) - RANGE="${LAST_DEV_TAG}..HEAD~1" + RANGE="${PREVIOUS_DEV_TAG}..HEAD~1" else # First dev release - check commits since last stable tag - STABLE_TAG=$(git tag -l "v[0-9]*.[0-9]*.[0-9]" --merged origin/main --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || echo "") RANGE="${STABLE_TAG:-HEAD~10}..HEAD~1" fi echo "Checking commits in range: $RANGE" - # Extract issue numbers from commit messages - ISSUES=$(git log $RANGE --pretty=format:"%s %b" 2>/dev/null | \ - grep -oE "(Fixes|Closes|Resolves|Refs?) #[0-9]+" | \ + COMMIT_TEXT=$(git log $RANGE --pretty=format:"%s%n%b" 2>/dev/null || true) + ISSUES_FROM_COMMITS=$(printf '%s\n' "$COMMIT_TEXT" | \ + perl -ne 'if (/(fixes|closes|resolves|refs?)(.*)/i) { print "$2\n"; }' | \ + grep -oE '#[0-9]+' || true) + + PR_CANDIDATES=$(printf '%s\n' "$COMMIT_TEXT" | \ + grep -oE "Merge pull request #[0-9]+|\\(#[0-9]+\\)" | \ + grep -oE "[0-9]+" | sort -u || true) + + PR_TEXT="" + for PR_NUM in $PR_CANDIDATES; do + DETAILS=$(gh pr view "$PR_NUM" --repo "${{ github.repository }}" --json title,body --jq '.title + "\n" + (.body // "")' 2>/dev/null || true) + if [[ -n "$DETAILS" ]]; then + PR_TEXT="${PR_TEXT}"$'\n'"${DETAILS}" + fi + done + + ISSUES_FROM_PRS=$(printf '%s\n' "$PR_TEXT" | \ + perl -ne 'if (/(fixes|closes|resolves|refs?)(.*)/i) { print "$2\n"; }' | \ + grep -oE '#[0-9]+' || true) + + ISSUES=$(printf '%s\n%s\n' "$ISSUES_FROM_COMMITS" "$ISSUES_FROM_PRS" | \ grep -oE "#[0-9]+" | sort -u || true) if [[ -z "$ISSUES" ]]; then diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 6d84b530..ad46ad84 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -1,17 +1,23 @@ name: Publish Docker Image on: - push: - tags: - - 'v*.*.*' + release: + types: + - published + workflow_dispatch: + inputs: + tag: + description: Stable tag to publish manually, for example v7.55.0 + required: true + type: string concurrency: - group: docker-release-${{ github.ref }} + group: docker-release-${{ github.event_name == 'release' && github.event.release.tag_name || inputs.tag || github.ref }} cancel-in-progress: false jobs: publish: - if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-') + if: ${{ github.event_name != 'release' || !github.event.release.prerelease }} runs-on: ubuntu-latest permissions: @@ -19,19 +25,36 @@ jobs: packages: write steps: - - name: Checkout release tag - uses: actions/checkout@v4 + - name: Resolve target tag + id: target + env: + MANUAL_TAG: ${{ inputs.tag }} + RELEASE_EVENT_TAG: ${{ github.event.release.tag_name }} + run: | + if [[ "${GITHUB_EVENT_NAME}" == "release" ]]; then + TARGET_TAG="${RELEASE_EVENT_TAG}" + else + TARGET_TAG="${MANUAL_TAG}" + fi + + echo "tag=${TARGET_TAG}" >> "$GITHUB_OUTPUT" - name: Validate stable semver tag id: tag run: | - if [[ "${GITHUB_REF_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + if [[ "${{ steps.target.outputs.tag }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "publish=true" >> "$GITHUB_OUTPUT" exit 0 fi echo "publish=false" >> "$GITHUB_OUTPUT" - echo "Skipping non-stable semver tag ${GITHUB_REF_NAME}" + echo "Skipping non-stable semver tag ${{ steps.target.outputs.tag }}" + + - name: Checkout release tag + if: steps.tag.outputs.publish == 'true' + uses: actions/checkout@v4 + with: + ref: ${{ steps.target.outputs.tag }} - name: Set up QEMU if: steps.tag.outputs.publish == 'true' @@ -44,18 +67,34 @@ jobs: - name: Derive image metadata if: steps.tag.outputs.publish == 'true' id: meta + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - VERSION="${GITHUB_REF_NAME#v}" + VERSION="${{ steps.target.outputs.tag }}" + VERSION="${VERSION#v}" MINOR="${VERSION%.*}" MAJOR="${VERSION%%.*}" OWNER_LOWER=$(echo "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]') IMAGE="ghcr.io/${OWNER_LOWER}/ccs-dashboard" + REVISION=$(git rev-parse HEAD) + TAGS="${IMAGE}:${VERSION}" + + if [[ "${GITHUB_EVENT_NAME}" == "release" ]]; then + TAGS="${TAGS} + ${IMAGE}:${MINOR} + ${IMAGE}:${MAJOR} + ${IMAGE}:latest" + fi { echo "version=${VERSION}" echo "minor=${MINOR}" echo "major=${MAJOR}" echo "image=${IMAGE}" + echo "revision=${REVISION}" + echo "tags<> "$GITHUB_OUTPUT" - name: Log in to GitHub Container Registry @@ -74,17 +113,13 @@ jobs: file: docker/Dockerfile platforms: linux/amd64,linux/arm64 push: true - tags: | - ${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.version }} - ${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.minor }} - ${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.major }} - ${{ steps.meta.outputs.image }}:latest + tags: ${{ steps.meta.outputs.tags }} labels: | org.opencontainers.image.title=ccs-dashboard org.opencontainers.image.description=CCS Dashboard container image org.opencontainers.image.url=https://github.com/${{ github.repository }} org.opencontainers.image.source=https://github.com/${{ github.repository }} org.opencontainers.image.version=${{ steps.meta.outputs.version }} - org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.revision=${{ steps.meta.outputs.revision }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.github/workflows/label-pending-release.yml b/.github/workflows/label-pending-release.yml index a966d113..dffe8a2d 100644 --- a/.github/workflows/label-pending-release.yml +++ b/.github/workflows/label-pending-release.yml @@ -2,7 +2,7 @@ name: Label Pending Release on: pull_request: - types: [opened, reopened] + types: [opened, reopened, edited, synchronize] branches: [dev] jobs: @@ -10,34 +10,62 @@ jobs: runs-on: ubuntu-latest permissions: issues: write + pull-requests: read steps: - - name: Label linked issues as pending-release + - name: Reconcile pending-release labels for open dev PRs env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_TITLE: ${{ github.event.pull_request.title }} - PR_BODY: ${{ github.event.pull_request.body }} run: | - # Extract issue numbers from PR title and body (passed via env vars for safety) - # Using env vars prevents shell injection from backticks in markdown - PR_TEXT="$PR_TITLE $PR_BODY" - ISSUES=$(echo "$PR_TEXT" | grep -oE "(Fixes|Closes|Resolves|Refs?) #[0-9]+" | grep -oE "#[0-9]+" | sort -u || true) + # Recompute the complete open-PR issue set so edited/synchronized PRs + # can remove stale pending-release labels as well as add new ones. + OPEN_PRS=$(gh pr list \ + --repo "${{ github.repository }}" \ + --base dev \ + --state open \ + --json number,title,body \ + --jq '.[] | @base64' 2>/dev/null || true) + + ALL_REFERENCED_ISSUES="" + while IFS= read -r PR_ROW; do + [[ -z "$PR_ROW" ]] && continue + PR_JSON=$(printf '%s' "$PR_ROW" | base64 --decode) + PR_NUM=$(printf '%s' "$PR_JSON" | jq -r '.number') + PR_TITLE=$(printf '%s' "$PR_JSON" | jq -r '.title') + PR_BODY=$(printf '%s' "$PR_JSON" | jq -r '.body // ""') + COMMIT_TEXT=$(gh api "repos/${{ github.repository }}/pulls/${PR_NUM}/commits" --paginate --jq '.[].commit.message' 2>/dev/null || true) + PR_TEXT="$PR_TITLE + $PR_BODY + $COMMIT_TEXT" + PR_ISSUES=$(printf '%s\n' "$PR_TEXT" | \ + perl -ne 'if (/(fixes|closes|resolves|refs?)(.*)/i) { print "$2\n"; }' | \ + grep -oE '#[0-9]+' || true) + if [[ -n "$PR_ISSUES" ]]; then + ALL_REFERENCED_ISSUES=$(printf '%s\n%s\n' "$ALL_REFERENCED_ISSUES" "$PR_ISSUES") + fi + done <<< "$OPEN_PRS" + + ISSUES=$(printf '%s\n' "$ALL_REFERENCED_ISSUES" | grep -oE '#[0-9]+' | sort -u || true) + CURRENT_PENDING=$(gh issue list --repo "${{ github.repository }}" --label "pending-release" --state all --json number --jq '.[].number' 2>/dev/null || true) + + gh label create "pending-release" \ + --color "fbca04" \ + --description "Fix in review, awaiting merge" \ + --repo ${{ github.repository }} 2>/dev/null || true if [[ -z "$ISSUES" ]]; then - echo "No linked issues found in PR" - exit 0 + echo "No linked issues found across open dev PRs" + else + for ISSUE in $ISSUES; do + NUM=${ISSUE#\#} + echo "Ensuring issue #$NUM has pending-release" + gh issue edit "$NUM" --add-label "pending-release" --repo "${{ github.repository }}" || true + done fi - for ISSUE in $ISSUES; do - NUM=${ISSUE#\#} - echo "Labeling issue #$NUM as pending-release" - - # Create label if doesn't exist - gh label create "pending-release" \ - --color "fbca04" \ - --description "Fix in review, awaiting merge" \ - --repo ${{ github.repository }} 2>/dev/null || true - - # Add label to issue - gh issue edit $NUM --add-label "pending-release" --repo ${{ github.repository }} || true + for NUM in $CURRENT_PENDING; do + if ! printf '%s\n' "$ISSUES" | grep -qx "#${NUM}"; then + echo "Removing stale pending-release from issue #$NUM" + gh issue edit "$NUM" --remove-label "pending-release" --repo "${{ github.repository }}" 2>/dev/null || true + fi done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3bec763a..5b0e2220 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,7 @@ on: jobs: release: + if: ${{ github.ref == 'refs/heads/main' }} runs-on: ubuntu-latest permissions: @@ -57,10 +58,17 @@ jobs: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | - OUTPUT=$(bunx semantic-release 2>&1) || true + set +e + OUTPUT=$(bunx semantic-release 2>&1) + STATUS=$? + set -e echo "$OUTPUT" # Strip ANSI color codes before matching (semantic-release outputs colored text) CLEAN=$(echo "$OUTPUT" | sed 's/\x1b\[[0-9;]*m//g') + if [[ "$STATUS" -ne 0 ]]; then + echo "released=false" >> $GITHUB_OUTPUT + exit "$STATUS" + fi if echo "$CLEAN" | grep -q "Published GitHub release"; then echo "released=true" >> $GITHUB_OUTPUT else @@ -83,31 +91,73 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - # semantic-release already adds "released" label via .releaserc.cjs - # This step removes transitional labels and closes issues now in stable. + VERSION=$(jq -r '.version' package.json) + RELEASE_BODY=$(gh release view "v${VERSION}" --repo "${{ github.repository }}" --json body --jq '.body' 2>/dev/null || echo "") + PREVIOUS_STABLE_TAG=$(git tag -l "v[0-9]*.[0-9]*.[0-9]" --sort=-v:refname | \ + grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | \ + grep -vx "v${VERSION}" | head -1 || echo "") + RANGE="${PREVIOUS_STABLE_TAG:+${PREVIOUS_STABLE_TAG}..HEAD~1}" + if [[ -z "$RANGE" ]]; then + RANGE="HEAD~50..HEAD~1" + fi - # Find issues with both "released" and "released-dev" labels - ISSUES=$(gh issue list --label "released" --label "released-dev" --state all --json number --jq '.[].number' 2>/dev/null || echo "") + COMMIT_TEXT=$(git log $RANGE --pretty=format:"%s%n%b" 2>/dev/null || true) + PR_CANDIDATES=$(printf '%s\n' "$COMMIT_TEXT" | \ + grep -oE "Merge pull request #[0-9]+|\\(#[0-9]+\\)" | \ + grep -oE "[0-9]+" | sort -u || true) - for NUM in $ISSUES; do - echo "Cleaning up labels on issue #$NUM" - gh issue edit "$NUM" --remove-label "released-dev" --remove-label "pending-release" --repo "${{ github.repository }}" 2>/dev/null || true + PR_TEXT="" + for PR_NUM in $PR_CANDIDATES; do + DETAILS=$(gh pr view "$PR_NUM" --repo "${{ github.repository }}" --json title,body --jq '.title + "\n" + (.body // "")' 2>/dev/null || true) + if [[ -n "$DETAILS" ]]; then + PR_TEXT="${PR_TEXT}"$'\n'"${DETAILS}" + fi done - # Also clean pending-release from any issues with released label - PENDING=$(gh issue list --label "released" --label "pending-release" --state all --json number --jq '.[].number' 2>/dev/null || echo "") + RELEASE_ISSUES_FROM_BODY=$(printf '%s\n' "$RELEASE_BODY" | \ + perl -ne 'if (/(fixes|closes|resolves|refs?)(.*)/i) { print "$2\n"; }' | \ + grep -oE '#[0-9]+' | tr -d '#' || true) + RELEASE_ISSUES_FROM_COMMITS=$(printf '%s\n' "$COMMIT_TEXT" | \ + perl -ne 'if (/(fixes|closes|resolves|refs?)(.*)/i) { print "$2\n"; }' | \ + grep -oE '#[0-9]+' | tr -d '#' || true) + RELEASE_ISSUES_FROM_PRS=$(printf '%s\n' "$PR_TEXT" | \ + perl -ne 'if (/(fixes|closes|resolves|refs?)(.*)/i) { print "$2\n"; }' | \ + grep -oE '#[0-9]+' | tr -d '#' || true) + RELEASE_ISSUES=$(printf '%s\n%s\n%s\n' \ + "$RELEASE_ISSUES_FROM_BODY" \ + "$RELEASE_ISSUES_FROM_COMMITS" \ + "$RELEASE_ISSUES_FROM_PRS" | sort -u || true) - for NUM in $PENDING; do - echo "Removing pending-release from issue #$NUM" - gh issue edit "$NUM" --remove-label "pending-release" --repo "${{ github.repository }}" 2>/dev/null || true - done + RESOLVED_ISSUES_FROM_BODY=$(printf '%s\n' "$RELEASE_BODY" | \ + perl -ne 'if (/(fixes|closes|resolves)(.*)/i) { print "$2\n"; }' | \ + grep -oE '#[0-9]+' | tr -d '#' || true) + RESOLVED_ISSUES_FROM_COMMITS=$(printf '%s\n' "$COMMIT_TEXT" | \ + perl -ne 'if (/(fixes|closes|resolves)(.*)/i) { print "$2\n"; }' | \ + grep -oE '#[0-9]+' | tr -d '#' || true) + RESOLVED_ISSUES_FROM_PRS=$(printf '%s\n' "$PR_TEXT" | \ + perl -ne 'if (/(fixes|closes|resolves)(.*)/i) { print "$2\n"; }' | \ + grep -oE '#[0-9]+' | tr -d '#' || true) + RESOLVED_ISSUES=$(printf '%s\n%s\n%s\n' \ + "$RESOLVED_ISSUES_FROM_BODY" \ + "$RESOLVED_ISSUES_FROM_COMMITS" \ + "$RESOLVED_ISSUES_FROM_PRS" | sort -u || true) - # Close open issues that are marked as released - OPEN_RELEASED=$(gh issue list --label "released" --state open --json number --jq '.[].number' 2>/dev/null || echo "") + if [[ -z "$RELEASE_ISSUES" ]]; then + echo "No release-scoped issues found for v${VERSION}" + exit 0 + fi - for NUM in $OPEN_RELEASED; do - echo "Closing released issue #$NUM" - gh issue close "$NUM" \ - --comment "[bot] Closing issue because this fix/feature is now in stable release (@latest)." \ - --repo "${{ github.repository }}" || true + for NUM in $RELEASE_ISSUES; do + echo "Cleaning release state on issue #$NUM" + gh issue edit "$NUM" \ + --remove-label "released-dev" \ + --remove-label "pending-release" \ + --repo "${{ github.repository }}" 2>/dev/null || true + + HAS_RELEASED=$(gh issue view "$NUM" --repo "${{ github.repository }}" --json labels --jq '[.labels[].name | select(. == "released")] | length' 2>/dev/null || echo "0") + if [[ "$HAS_RELEASED" -gt 0 ]] && printf '%s\n' "$RESOLVED_ISSUES" | grep -qx "$NUM"; then + gh issue close "$NUM" \ + --comment "[bot] Closing issue because this fix/feature is now in stable release (@latest)." \ + --repo "${{ github.repository }}" || true + fi done diff --git a/.github/workflows/sync-dev-after-release.yml b/.github/workflows/sync-dev-after-release.yml index b89d1cdf..55ec94b5 100644 --- a/.github/workflows/sync-dev-after-release.yml +++ b/.github/workflows/sync-dev-after-release.yml @@ -1,15 +1,12 @@ name: Sync Dev After Main Release on: - workflow_run: - workflows: ["Release"] - types: [completed] - branches: [main] + release: + types: [published] jobs: sync-dev: - # Only run if Release workflow succeeded on main branch - if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.head_branch == 'main' }} + if: ${{ !github.event.release.prerelease && github.event.release.target_commitish == 'main' && startsWith(github.event.release.tag_name, 'v') && !contains(github.event.release.tag_name, '-') }} runs-on: ubuntu-latest permissions: diff --git a/scripts/dev-release.sh b/scripts/dev-release.sh index 1fb74bc3..5b372216 100755 --- a/scripts/dev-release.sh +++ b/scripts/dev-release.sh @@ -26,8 +26,8 @@ log_info() { echo -e "${GREEN}[i]${NC} $1"; } log_warn() { echo -e "${YELLOW}[!]${NC} $1"; } log_error() { echo -e "${RED}[X]${NC} $1"; } -# Ensure we have the latest tags -git fetch --tags origin main +# Ensure we have the latest tags and branch refs +git fetch --tags origin main dev # Get latest stable tag from main (exclude prereleases like -dev, -beta, -rc) # Match only clean semver tags: vX.Y.Z @@ -39,58 +39,100 @@ if [ -z "$STABLE_TAG" ]; then fi STABLE=${STABLE_TAG#v} +CURRENT_VERSION=$(jq -r '.version' package.json) +HEAD_SUBJECT=$(git log -1 --pretty=%s 2>/dev/null || echo "") +DEV_VERSION_REGEX="^${STABLE//./\\.}-dev\\.[0-9]+$" +PREVIOUS_DEV_TAG="" +RECOVERY_MODE=false + log_info "Current stable version: ${STABLE}" -# Find latest dev tag for this stable version -LATEST_DEV=$(git tag -l "v${STABLE}-dev.*" --sort=-v:refname | head -1 || echo "") +if [[ "$CURRENT_VERSION" =~ $DEV_VERSION_REGEX ]] && [[ "$HEAD_SUBJECT" == "chore(release): ${CURRENT_VERSION} [skip ci]" ]]; then + VERSION="$CURRENT_VERSION" + CURRENT_TAG="v${VERSION}" + PREVIOUS_DEV_TAG=$(git tag -l "v${STABLE}-dev.*" --sort=-v:refname | grep -vx "$CURRENT_TAG" | head -1 || echo "") + RECOVERY_MODE=true + log_warn "Recovery mode for ${VERSION}" -# Calculate next dev number -if [ -z "$LATEST_DEV" ]; then - DEV_NUM=1 - log_info "No existing dev tags for ${STABLE}, starting at dev.1" + if git rev-parse "${CURRENT_TAG}" >/dev/null 2>&1; then + TAG_COMMIT=$(git rev-parse "${CURRENT_TAG}^{commit}") + HEAD_COMMIT=$(git rev-parse HEAD) + if [[ "$TAG_COMMIT" != "$HEAD_COMMIT" ]]; then + log_error "Tag ${CURRENT_TAG} exists but does not point to HEAD" + exit 1 + fi + log_info "Reusing existing tag ${CURRENT_TAG}" + else + git tag "${CURRENT_TAG}" + log_warn "Recreated missing tag ${CURRENT_TAG} on release commit" + fi else - DEV_NUM=$(echo "$LATEST_DEV" | sed 's/.*dev\.\([0-9]*\)/\1/') - DEV_NUM=$((DEV_NUM + 1)) - log_info "Latest dev tag: ${LATEST_DEV}, incrementing to dev.${DEV_NUM}" + # Find latest dev tag for this stable version + LATEST_DEV=$(git tag -l "v${STABLE}-dev.*" --sort=-v:refname | head -1 || echo "") + PREVIOUS_DEV_TAG="$LATEST_DEV" + + # Calculate next dev number + if [ -z "$LATEST_DEV" ]; then + DEV_NUM=1 + log_info "No existing dev tags for ${STABLE}, starting at dev.1" + else + DEV_NUM=$(echo "$LATEST_DEV" | sed 's/.*dev\.\([0-9]*\)/\1/') + DEV_NUM=$((DEV_NUM + 1)) + log_info "Latest dev tag: ${LATEST_DEV}, incrementing to dev.${DEV_NUM}" + fi + + VERSION="${STABLE}-dev.${DEV_NUM}" + CURRENT_TAG="v${VERSION}" + log_info "New version: ${VERSION}" + + # Check if tag already exists (safety check) + if git rev-parse "${CURRENT_TAG}" >/dev/null 2>&1; then + log_error "Tag ${CURRENT_TAG} already exists!" + exit 1 + fi + + # Update package.json + npm version "$VERSION" --no-git-tag-version + log_info "Updated package.json to ${VERSION}" + + # Configure git for GitHub Actions + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Commit version change + git add package.json + git commit -m "chore(release): ${VERSION} [skip ci]" + log_info "Created release commit" + + # Create tag + git tag "${CURRENT_TAG}" + log_info "Created tag ${CURRENT_TAG}" fi -VERSION="${STABLE}-dev.${DEV_NUM}" -log_info "New version: ${VERSION}" - -# Check if tag already exists (safety check) -if git rev-parse "v${VERSION}" >/dev/null 2>&1; then - log_error "Tag v${VERSION} already exists!" - exit 1 +PACKAGE_NAME=$(jq -r '.name' package.json) +if npm view "${PACKAGE_NAME}@${VERSION}" version >/dev/null 2>&1; then + log_info "npm already has ${PACKAGE_NAME}@${VERSION}, skipping publish" +else + npm publish --tag dev + log_info "Published to npm with @dev tag" fi -# Update package.json -npm version "$VERSION" --no-git-tag-version -log_info "Updated package.json to ${VERSION}" +if git merge-base --is-ancestor HEAD origin/dev >/dev/null 2>&1; then + log_info "origin/dev already contains release commit" +else + git push origin HEAD:dev + log_info "Pushed release commit to origin/dev" +fi -# Configure git for GitHub Actions -git config user.name "github-actions[bot]" -git config user.email "github-actions[bot]@users.noreply.github.com" - -# Commit version change -git add package.json -git commit -m "chore(release): ${VERSION} [skip ci]" -log_info "Created release commit" - -# Create tag -git tag "v${VERSION}" -log_info "Created tag v${VERSION}" - -# Push commit and tag -git push origin dev -git push origin "v${VERSION}" -log_info "Pushed to origin" - -# Publish to npm with dev tag -npm publish --tag dev -log_info "Published to npm with @dev tag" +if git ls-remote --exit-code --tags origin "refs/tags/${CURRENT_TAG}" >/dev/null 2>&1; then + log_info "Remote tag ${CURRENT_TAG} already exists" +else + git push origin "${CURRENT_TAG}" + log_info "Pushed tag ${CURRENT_TAG}" +fi # Generate release notes from commits since last tag -PREV_TAG=$(git tag -l "v*" --sort=-v:refname | sed -n '2p' || echo "") +PREV_TAG="${PREVIOUS_DEV_TAG:-$STABLE_TAG}" if [ -n "$PREV_TAG" ]; then # Get commits between previous tag and the one before our release commit NOTES=$(git log --pretty=format:"- %s" "${PREV_TAG}..HEAD~1" 2>/dev/null | grep -v "chore(release):" | head -15 || echo "- Dev release") @@ -99,12 +141,16 @@ else fi # Create GitHub prerelease -gh release create "v${VERSION}" \ - --title "v${VERSION}" \ - --notes "${NOTES}" \ - --prerelease +if gh release view "${CURRENT_TAG}" >/dev/null 2>&1; then + log_info "GitHub prerelease ${CURRENT_TAG} already exists" +else + gh release create "${CURRENT_TAG}" \ + --title "${CURRENT_TAG}" \ + --notes "${NOTES}" \ + --prerelease -log_info "Created GitHub prerelease" + log_info "Created GitHub prerelease" +fi # Save release info for Discord notification # This file is read by send-discord-release.cjs for dev releases @@ -117,7 +163,13 @@ EOF log_info "Saved release info for Discord notification" # Output for GitHub Actions -echo "version=${VERSION}" >> "${GITHUB_OUTPUT:-/dev/null}" -echo "released=true" >> "${GITHUB_OUTPUT:-/dev/null}" +{ + echo "current_tag=${CURRENT_TAG}" + echo "previous_dev_tag=${PREVIOUS_DEV_TAG}" + echo "recovery_mode=${RECOVERY_MODE}" + echo "released=true" + echo "stable_tag=${STABLE_TAG}" + echo "version=${VERSION}" +} >> "${GITHUB_OUTPUT:-/dev/null}" log_info "Dev release ${VERSION} complete!"