fix(ci): harden release automation workflows

This commit is contained in:
Tam Nhu Tran committed 2026-03-23 10:53:51 -04:00
1 parent 2ee87a4532
commit 5616c68471
6 files changed
+223 -119

No files matched your search

+27 -15
View File
@@ -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:
@@ -65,12 +64,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 +81,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 | \
COMMIT_TEXT=$(git log $RANGE --pretty=format:"%s%n%b" 2>/dev/null || true)
ISSUES_FROM_COMMITS=$(printf '%s\n' "$COMMIT_TEXT" | \
grep -oE "(Fixes|Closes|Resolves|Refs?) #[0-9]+" | \
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" | \
grep -oE "(Fixes|Closes|Resolves|Refs?) #[0-9]+" | \
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
+48 -18
View File
@@ -4,39 +4,57 @@ on:
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: ${{ !github.event.release.prerelease && startsWith(github.event.release.tag_name, 'v') && !contains(github.event.release.tag_name, '-') }}
if: ${{ github.event_name != 'release' || !github.event.release.prerelease }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
steps:
- name: Checkout release tag
uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }}
- 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 [[ "${RELEASE_TAG}" =~ ^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 ${RELEASE_TAG}"
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'
@@ -49,18 +67,34 @@ jobs:
- name: Derive image metadata
if: steps.tag.outputs.publish == 'true'
id: meta
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${RELEASE_TAG#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}
${IMAGE}:${MINOR}
${IMAGE}:${MAJOR}"
if [[ "${GITHUB_EVENT_NAME}" == "release" ]]; then
TAGS="${TAGS}
${IMAGE}:latest"
fi
{
echo "version=${VERSION}"
echo "minor=${MINOR}"
echo "major=${MAJOR}"
echo "image=${IMAGE}"
echo "revision=${REVISION}"
echo "tags<<EOF"
echo "${TAGS}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Log in to GitHub Container Registry
@@ -79,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
+9 -4
View File
@@ -2,7 +2,7 @@ name: Label Pending Release
on:
pull_request:
types: [opened, reopened]
types: [opened, reopened, edited, synchronize]
branches: [dev]
jobs:
@@ -10,17 +10,22 @@ jobs:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: read
steps:
- name: Label linked issues as pending-release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
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"
# Extract issue numbers from PR title/body plus current PR commits.
# Using env vars prevents shell injection from backticks in markdown.
COMMIT_TEXT=$(gh api "repos/${{ github.repository }}/pulls/${PR_NUMBER}/commits" --paginate --jq '.[].commit.message' 2>/dev/null || true)
PR_TEXT="$PR_TITLE
$PR_BODY
$COMMIT_TEXT"
ISSUES=$(echo "$PR_TEXT" | grep -oE "(Fixes|Closes|Resolves|Refs?) #[0-9]+" | grep -oE "#[0-9]+" | sort -u || true)
if [[ -z "$ISSUES" ]]; then
+34 -26
View File
@@ -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,31 @@ 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 "")
RELEASE_ISSUES=$(printf '%s\n' "$RELEASE_BODY" | \
perl -ne 'if (/(fixes|closes|resolves|refs?)(.*)/i) { print "$2\n"; }' | \
grep -oE '#[0-9]+' | tr -d '#' | sort -u || true)
RESOLVED_ISSUES=$(printf '%s\n' "$RELEASE_BODY" | \
perl -ne 'if (/(fixes|closes|resolves)(.*)/i) { print "$2\n"; }' | \
grep -oE '#[0-9]+' | tr -d '#' | sort -u || true)
# 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 "")
if [[ -z "$RELEASE_ISSUES" ]]; then
echo "No release-scoped issues found for v${VERSION}"
exit 0
fi
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
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 "")
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
# 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 "")
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
+3 -6
View File
@@ -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: