fix(ci): improve changed projects detection script in release workflow #151
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Auto Bump Version, Build & Publish | |
| on: | |
| push: | |
| branches: | |
| - main | |
| jobs: | |
| bump: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| packages: write | |
| env: | |
| MY_DASHBOARD_DATABASE_POSTGRES_URL: "postgres://postgres:pgtoolspassword@localhost:5432/postgres?schema=public" | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24.11.1 | |
| # 🔍 Проверяем наличие [need ci] | |
| - name: Check if [need ci] present | |
| id: need_ci | |
| run: | | |
| if git log ${{ github.event.before }}..${{ github.sha }} --pretty=format:"%s%b" | grep -q "\[need ci\]"; then | |
| echo "force_run=true" >> $GITHUB_OUTPUT | |
| echo "✅ Found [need ci] — forcing full pipeline" | |
| else | |
| echo "force_run=false" >> $GITHUB_OUTPUT | |
| echo "ℹ️ No [need ci] found" | |
| fi | |
| # 🔍 Определяем изменённые проекты | |
| - name: Detect changed projects | |
| id: detect | |
| run: | | |
| echo "Starting project detection..." | |
| PROJECTS=$(find . -maxdepth 1 -type d ! -path "." ! -path "./.github*" ! -path "./.vscode*" ! -path "./scripts*" ! -path "./demo*" | while read d; do | |
| if [ -f "$d/package.json" ]; then echo "$(basename "$d")"; fi | |
| done) | |
| echo "Found projects: '$PROJECTS'" | |
| # Handle case where no projects are found | |
| if [ -z "$PROJECTS" ]; then | |
| echo "No projects found with package.json files" | |
| PROJECTS="" | |
| fi | |
| if [ "${{ steps.need_ci.outputs.force_run }}" = "true" ]; then | |
| CHANGED_PROJECTS="$PROJECTS" | |
| echo "Force run enabled, using all projects" | |
| else | |
| echo "Checking for changed files..." | |
| CHANGED=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} | cut -d/ -f1 | sort | uniq | grep -v -E '^(.github|.vscode|scripts|demo)$') | |
| echo "Changed directories: '$CHANGED'" | |
| if [ -n "$CHANGED" ] && [ -n "$PROJECTS" ]; then | |
| echo "Filtering projects based on changes..." | |
| CHANGED_PROJECTS=$(echo "$PROJECTS" | grep -Fxf <(echo "$CHANGED") || true) | |
| else | |
| echo "No changes or no projects found, setting empty" | |
| CHANGED_PROJECTS="" | |
| fi | |
| fi | |
| echo "Changed projects: $CHANGED_PROJECTS" | |
| CHANGED_PROJECTS_CLEAN=$(echo "$CHANGED_PROJECTS" | tr '\n' ' ' | sed 's/ $//' | sed 's/^[[:space:]]*//') | |
| echo "Changed projects clean: '$CHANGED_PROJECTS_CLEAN'" | |
| # Use printf instead of echo -n for better compatibility | |
| ENCODED=$(printf "%s" "$CHANGED_PROJECTS_CLEAN" | base64 | tr -d '\n') | |
| echo "changed_projects_base64=$ENCODED" >> $GITHUB_OUTPUT | |
| echo "Encoded value: $ENCODED" | |
| # 🧩 Определяем тип bump'а | |
| - name: Determine bump type | |
| id: bump_type | |
| run: | | |
| MESSAGES=$(git log ${{ github.event.before }}..${{ github.sha }} --pretty=format:"%s%n%b") | |
| if echo "$MESSAGES" | grep -qi "BREAKING CHANGE"; then | |
| BUMP=major | |
| elif echo "$MESSAGES" | grep -Eqi "^feat"; then | |
| BUMP=minor | |
| elif echo "$MESSAGES" | grep -Eqi "^fix"; then | |
| BUMP=patch | |
| else | |
| BUMP=none | |
| fi | |
| echo "bump=$BUMP" >> $GITHUB_OUTPUT | |
| echo "Detected bump type: $BUMP" | |
| # 🚀 Bump, Build & Docker | |
| - name: Bump, Build & Docker | |
| if: steps.need_ci.outputs.force_run == 'true' || (steps.bump_type.outputs.bump != 'none' && steps.detect.outputs.changed_projects_base64 != '') | |
| id: bump_versions | |
| env: | |
| DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USERNAME }} | |
| DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} | |
| run: | | |
| sudo apt-get install -y jq | |
| npm install -g conventional-changelog-cli | |
| if [ -n "${DOCKERHUB_USER}" ] && [ -n "${DOCKERHUB_TOKEN}" ]; then | |
| echo "${DOCKERHUB_TOKEN}" | docker login -u "${DOCKERHUB_USER}" --password-stdin | |
| else | |
| echo "⚠️ DockerHub credentials missing — skipping Docker publish" | |
| fi | |
| BUMP=${{ steps.bump_type.outputs.bump }} | |
| CHANGED_PROJECTS=$(echo "${{ steps.detect.outputs.changed_projects_base64 }}" | base64 --decode) | |
| if [ -z "$CHANGED_PROJECTS" ]; then | |
| echo "ℹ️ No changed projects detected. Exiting." | |
| exit 0 | |
| fi | |
| TAGS="" | |
| cd web && npm ci && cd .. | |
| for project in $CHANGED_PROJECTS; do | |
| if [ -f "$project/package.json" ]; then | |
| cd $project | |
| NAME=$(jq -r '.name' package.json) | |
| DESC=$(jq -r '.description // "No description"' package.json) | |
| OLD_VERSION=$(jq -r '.version' package.json) | |
| if [ "$BUMP" != "none" ]; then | |
| npm version $BUMP --no-git-tag-version | |
| fi | |
| NEW_VERSION=$(jq -r '.version' package.json) | |
| conventional-changelog -p angular -i CHANGELOG.md -s -r 1 || true | |
| if jq -e '.scripts.build' package.json >/dev/null; then | |
| npm ci | |
| npm run build || echo "⚠️ Build failed for $project" | |
| fi | |
| if [ -f "Dockerfile" ] && [ -n "${DOCKERHUB_USER}" ]; then | |
| IMAGE_NAME="${DOCKERHUB_USER}/${NAME}" | |
| docker build -t "$IMAGE_NAME:$NEW_VERSION" -t "$IMAGE_NAME:latest" \ | |
| --label "org.opencontainers.image.title=$NAME" \ | |
| --label "org.opencontainers.image.description=$DESC" \ | |
| --label "org.opencontainers.image.version=$NEW_VERSION" \ | |
| --label "org.opencontainers.image.source=https://github.com/${{ github.repository }}" \ | |
| . || true | |
| docker push "$IMAGE_NAME:$NEW_VERSION" || true | |
| docker push "$IMAGE_NAME:latest" || true | |
| fi | |
| if [ -f "capacitor.config.ts" ]; then | |
| echo "${{ secrets.KEYSTORE }}" | base64 --decode > "${{ github.workspace }}/$project/my-dashboard.jks" | |
| docker run --rm \ | |
| -e KEYSTORE_PASSWORD="${{ secrets.KEYSTORE_PASSWORD }}" \ | |
| -e KEYSTORE_ALIAS="${{ secrets.KEYSTORE_ALIAS }}" \ | |
| -e KEYSTORE_ALIAS_PASSWORD="${{ secrets.KEYSTORE_ALIAS_PASSWORD }}" \ | |
| -e PRISMA_ENGINES_MIRROR="https://registry.npmmirror.com/-/binary/prisma" \ | |
| -v "${{ github.workspace }}/$project:/app" \ | |
| endykaufman/ionic-capacitor:latest | |
| mkdir -p ${{ github.workspace }}/$project/artifacts | |
| find ${{ github.workspace }}/$project/android/app/build/outputs/apk/release -type f -name "*.apk" -exec cp {} ${{ github.workspace }}/$project/artifacts/ \; | |
| fi | |
| cd - | |
| TAG="${project}@${NEW_VERSION}" | |
| git rev-parse "$TAG" >/dev/null 2>&1 || git tag "$TAG" | |
| TAGS="$TAGS $TAG" | |
| fi | |
| done | |
| echo "tags=$TAGS" >> $GITHUB_OUTPUT | |
| # 💾 Commit and push changes | |
| - name: Commit and push changes | |
| if: steps.need_ci.outputs.force_run == 'true' || (steps.bump_type.outputs.bump != 'none' && steps.detect.outputs.changed_projects_base64 != '') | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add */package.json */CHANGELOG.md || true | |
| git commit -m "chore: auto bump version [skip ci]" || echo "No changes" | |
| git push origin main | |
| git push origin --tags | |
| # 📦 Upload artifacts | |
| - name: Upload artifacts | |
| if: steps.need_ci.outputs.force_run == 'true' || steps.bump_versions.outputs.tags != '' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: project-builds | |
| path: "**/artifacts/**" | |
| if-no-files-found: ignore | |
| - name: Create releases & send Telegram message | |
| if: steps.need_ci.outputs.force_run == 'true' || steps.bump_versions.outputs.tags != '' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USERNAME }} | |
| TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} | |
| run: | | |
| # --- функция для экранирования Markdown-спецсимволов | |
| escape_markdown() { | |
| local text="$1" | |
| text="${text//_/\\_}" | |
| text="${text//\*/\\*}" | |
| text="${text//~/\\~}" | |
| echo "$text" | |
| } | |
| # --- проверка [hidden] | |
| if git log ${{ github.event.before }}..${{ github.sha }} --pretty=format:"%s%b" | grep -q "\[hidden\]"; then | |
| SKIP_TELEGRAM=1 | |
| else | |
| SKIP_TELEGRAM=0 | |
| fi | |
| TAGS="${{ steps.bump_versions.outputs.tags }}" | |
| if [ -z "$TAGS" ] && [ "${{ steps.need_ci.outputs.force_run }}" = "true" ]; then | |
| TAGS=$(find . -maxdepth 1 -type d ! -path "." ! -path "./.github*" ! -path "./.vscode*" ! -path "./scripts*" ! -path "./demo*" -exec bash -c 'cd "{}" && if [ -f package.json ]; then NAME=$(jq -r .name package.json); VER=$(jq -r .version package.json); echo "$(basename "$PWD")@$VER"; fi' \;) | |
| fi | |
| for TAG in $TAGS; do | |
| PROJECT=$(echo "$TAG" | cut -d@ -f1) | |
| PKG="$PROJECT/package.json" | |
| [ ! -f "$PKG" ] && continue | |
| NAME=$(jq -r '.name' "$PKG") | |
| VERSION=$(jq -r '.version' "$PKG") | |
| DESC=$(jq -r '.description // "No description"' "$PKG") | |
| CHANGELOG_PATH="$PROJECT/CHANGELOG.md" | |
| ARTIFACTS_PATH="$PROJECT/artifacts" | |
| RELEASE_URL="https://github.com/${{ github.repository }}/releases/tag/${TAG}" | |
| PROJECT_URL="https://github.com/${{ github.repository }}/tree/main/${PROJECT}" | |
| PIPELINE_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| # --- GitHub release через файл с переносами | |
| if [ -f "$CHANGELOG_PATH" ]; then | |
| BODY=$(git log --pretty=format:"• %s" ${{ github.event.before }}..${{ github.sha }} -- "$PROJECT") | |
| [ -z "$BODY" ] && BODY=$(git log -n 5 --pretty=format:"• %s") | |
| else | |
| BODY=$(git log --pretty=format:"• %s" ${{ github.event.before }}..${{ github.sha }} -- "$PROJECT") | |
| fi | |
| NOTES="🚀 ${NAME} v${VERSION} released! | |
| Changelog: | |
| ${BODY} | |
| " | |
| echo "$NOTES" > release_notes.txt | |
| echo "ARTIFACTS_PATH: $ARTIFACTS_PATH" | |
| if [ -d "$ARTIFACTS_PATH" ]; then | |
| FILES=$(find "$ARTIFACTS_PATH" -type f) | |
| echo "FILES: $FILES" | |
| gh release create "$TAG" --title "$TAG" --notes-file release_notes.txt $FILES || echo "Release exists" | |
| else | |
| gh release create "$TAG" --title "$TAG" --notes-file release_notes.txt || echo "Release exists" | |
| fi | |
| # --- Telegram | |
| if [ "$SKIP_TELEGRAM" -eq 0 ]; then | |
| TG_BODY=$(echo -e "🚀 ${NAME} v${VERSION} released!%0A%0A_${DESC}_%0A%0A") | |
| if [ -f "$CHANGELOG_PATH" ]; then | |
| BODY=$(git log --pretty=format:"• %s" ${{ github.event.before }}..${{ github.sha }} -- "$PROJECT") | |
| [ -z "$BODY" ] && BODY=$(git log -n 5 --pretty=format:"• %s") | |
| # Заменяем переносы на %0A | |
| NEW_TG_NOTES=$(echo "$BODY" | sed ':a;N;$!ba;s/\n/%0A/g') | |
| # Экранирование спецсимволов | |
| NEW_TG_NOTES=$(escape_markdown "$NEW_TG_NOTES") | |
| TG_BODY+="*Changelog:*%0A${NEW_TG_NOTES}%0A" | |
| else | |
| BODY=$(git log --pretty=format:"• %s" ${{ github.event.before }}..${{ github.sha }} -- "$PROJECT") | |
| # Заменяем переносы на %0A | |
| NEW_TG_NOTES=$(echo "$BODY" | sed ':a;N;$!ba;s/\n/%0A/g') | |
| # Экранирование спецсимволов | |
| NEW_TG_NOTES=$(escape_markdown "$NEW_TG_NOTES") | |
| TG_BODY+="*Recent commits:*%0A${NEW_TG_NOTES}%0A" | |
| fi | |
| BUTTONS="[" | |
| if [ -d "$ARTIFACTS_PATH" ]; then | |
| while IFS= read -r FILE; do | |
| EXT="${FILE##*.}" | |
| if [[ "$EXT" =~ ^(apk|exe|zip|html)$ ]]; then | |
| BASENAME=$(basename "$FILE") | |
| SAFE_TAG=$(echo "$TAG" | sed 's/@/%40/g') # <-- заменяем @ на %40 | |
| FILE_URL="https://github.com/${{ github.repository }}/releases/download/${SAFE_TAG}/${BASENAME}" | |
| BUTTONS="${BUTTONS}[{\"text\":\"💾 ${BASENAME}\",\"url\":\"${FILE_URL}\"}]," | |
| fi | |
| done < <(find "$ARTIFACTS_PATH" -type f) | |
| fi | |
| if [ -f "$PROJECT/Dockerfile" ]; then | |
| BUTTONS="${BUTTONS}[{\"text\":\"🐳 Docker image\",\"url\":\"https://hub.docker.com/r/${DOCKERHUB_USER}/${NAME}\"}]," | |
| fi | |
| HOMEPAGE=$(jq -r '.homepage // empty' "$PKG") | |
| if [ -n "$HOMEPAGE" ]; then | |
| BUTTONS="${BUTTONS}[{\"text\":\"🌐 Homepage\",\"url\":\"${HOMEPAGE}\"}]," | |
| fi | |
| BUTTONS="${BUTTONS}[{\"text\":\"📂 Project folder\",\"url\":\"${PROJECT_URL}\"}]," | |
| BUTTONS="${BUTTONS}[{\"text\":\"ℹ️ View release\",\"url\":\"${RELEASE_URL}\"}]" | |
| BUTTONS="${BUTTONS}]" | |
| BUTTONS=$(echo "$BUTTONS" | sed 's/,]/]/') | |
| echo "TG_BODY: $TG_BODY" | |
| echo "BUTTONS: $BUTTONS" | |
| # Отправка Telegram с обработкой ошибок | |
| if ! curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ | |
| -d "chat_id=${{ vars.TELEGRAM_CHAT_ID }}" \ | |
| -d "message_thread_id=${{ vars.TELEGRAM_CHAT_THREAD_ID }}" \ | |
| -d "parse_mode=Markdown" \ | |
| -d "disable_web_page_preview=true" \ | |
| -d "text=${TG_BODY}" \ | |
| -d "reply_markup={\"inline_keyboard\":${BUTTONS}}"; then | |
| echo "⚠️ Telegram send failed!" | |
| echo "TG_BODY: $TG_BODY" | |
| echo "BUTTONS: $BUTTONS" | |
| fi | |
| fi | |
| done |