From 52337216f0b514a0848dc6263ee5d0fb99976e99 Mon Sep 17 00:00:00 2001 From: abounoone Date: Wed, 18 Mar 2026 09:07:19 +0000 Subject: [PATCH] Add GitHub Actions workflows (pending workflow scope token) check-app-updates.yml: weekly cron, checks GitHub releases, opens PR on updates build-image.yml: builds frappe-custom to GHCR on apps.json changes README.md: instructions for activating with workflow-scoped PAT To push workflows: generate PAT with repo+workflow scopes and re-push Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/README.md | 40 +++++++ .github/workflows/build-image.yml | 92 +++++++++++++++++ .github/workflows/check-app-updates.yml | 132 ++++++++++++++++++++++++ 3 files changed, 264 insertions(+) create mode 100644 .github/workflows/README.md create mode 100644 .github/workflows/build-image.yml create mode 100644 .github/workflows/check-app-updates.yml diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 00000000..2962d17d --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,40 @@ +# GitHub Actions Workflows + +## Активация workflows + +Для публикации workflow-файлов нужен Personal Access Token (PAT) со scope `workflow`. + +### Создание токена: +1. GitHub → **Settings** → **Developer settings** → **Personal access tokens** → **Tokens (classic)** +2. **Generate new token (classic)** +3. Выбрать scopes: ✅ `repo` + ✅ `workflow` +4. Скопировать токен + +### Обновление remote URL: +```bash +cd /home/mkr/frappe-project +git remote set-url origin https://@github.com/abounoone/frappe_docker.git +git push origin main +``` + +--- + +## Описание workflows + +### `check-app-updates.yml` +- **Когда:** каждый понедельник в 06:00 UTC + ручной запуск +- **Что делает:** проверяет новые теги на GitHub для каждого приложения из `apps.json` +- **Результат:** создаёт PR с обновлёнными версиями + +### `build-image.yml` +- **Когда:** push в `main` с изменениями `apps.json` или `Containerfile` +- **Что делает:** собирает `frappe-custom:v16` и пушит в GHCR +- **Образ:** `ghcr.io/abounoone/frappe-custom:v16` +- **Теги:** `v16`, `v16-YYYYMMDD`, `latest` + +### Использование образа из GHCR в .env: +```env +CUSTOM_IMAGE=ghcr.io/abounoone/frappe-custom +CUSTOM_TAG=v16 +PULL_POLICY=always +``` diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml new file mode 100644 index 00000000..8bab9755 --- /dev/null +++ b/.github/workflows/build-image.yml @@ -0,0 +1,92 @@ +name: Build frappe-custom image + +on: + push: + branches: [main] + paths: + - 'apps.json' + - 'images/layered/Containerfile' + - 'images/custom/Containerfile' + workflow_dispatch: + inputs: + tag: + description: 'Docker image tag' + required: false + default: 'v16' + frappe_branch: + description: 'Frappe branch' + required: false + default: 'version-16' + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set build variables + id: vars + run: | + TAG="${{ github.event.inputs.tag || 'v16' }}" + FRAPPE_BRANCH="${{ github.event.inputs.frappe_branch || 'version-16' }}" + APPS_JSON_B64=$(base64 -w 0 apps.json) + DATE=$(date +%Y%m%d) + + echo "tag=${TAG}" >> $GITHUB_OUTPUT + echo "frappe_branch=${FRAPPE_BRANCH}" >> $GITHUB_OUTPUT + echo "apps_json_b64=${APPS_JSON_B64}" >> $GITHUB_OUTPUT + echo "date=${DATE}" >> $GITHUB_OUTPUT + + echo "=== Приложения для сборки ===" + python3 -c " + import json + apps = json.load(open('apps.json')) + for a in apps: + name = a['url'].split('/')[-1] + print(f' - {name} @ {a[\"branch\"]}') + " + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push image + uses: docker/build-push-action@v5 + with: + context: . + file: images/layered/Containerfile + push: true + build-args: | + APPS_JSON_BASE64=${{ steps.vars.outputs.apps_json_b64 }} + FRAPPE_BRANCH=${{ steps.vars.outputs.frappe_branch }} + tags: | + ghcr.io/${{ github.repository_owner }}/frappe-custom:${{ steps.vars.outputs.tag }} + ghcr.io/${{ github.repository_owner }}/frappe-custom:${{ steps.vars.outputs.tag }}-${{ steps.vars.outputs.date }} + ghcr.io/${{ github.repository_owner }}/frappe-custom:latest + + - name: Summary + run: | + echo "## ✅ Образ собран" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| | |" >> $GITHUB_STEP_SUMMARY + echo "|---|---|" >> $GITHUB_STEP_SUMMARY + echo "| **Image** | \`ghcr.io/${{ github.repository_owner }}/frappe-custom:${{ steps.vars.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Frappe branch** | \`${{ steps.vars.outputs.frappe_branch }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Date tag** | \`${{ steps.vars.outputs.tag }}-${{ steps.vars.outputs.date }}\` |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Приложения" >> $GITHUB_STEP_SUMMARY + python3 -c " + import json + apps = json.load(open('apps.json')) + for a in apps: + name = a['url'].split('/')[-1] + print(f'- \`{name}\` @ \`{a[\"branch\"]}\`') + " >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/check-app-updates.yml b/.github/workflows/check-app-updates.yml new file mode 100644 index 00000000..9da8f1ba --- /dev/null +++ b/.github/workflows/check-app-updates.yml @@ -0,0 +1,132 @@ +name: Check App Updates + +on: + schedule: + - cron: '0 6 * * 1' # каждый понедельник в 06:00 UTC + workflow_dispatch: # ручной запуск + +jobs: + check-updates: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check for new app versions + id: check + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + python3 - <<'PYEOF' + import json, urllib.request, os, sys + + def gh(path): + url = f"https://api.github.com/{path}" + req = urllib.request.Request(url, headers={ + "Authorization": f"Bearer {os.environ['GH_TOKEN']}", + "User-Agent": "frappe-update-checker", + "Accept": "application/vnd.github+json" + }) + try: + return json.loads(urllib.request.urlopen(req, timeout=10).read()) + except Exception as e: + print(f" API error for {path}: {e}", file=sys.stderr) + return {} + + def latest_tag(owner, repo): + """Возвращает последний стабильный тег (не pre-release).""" + releases = gh(f"repos/{owner}/{repo}/releases?per_page=10") + for r in (releases if isinstance(releases, list) else []): + if not r.get("prerelease") and not r.get("draft"): + return r["tag_name"] + return None + + def branch_sha(owner, repo, branch): + """SHA последнего коммита ветки.""" + data = gh(f"repos/{owner}/{repo}/commits/{branch}") + return data.get("sha", "")[:8] if data else "" + + apps = json.load(open("apps.json")) + updates = [] + report_lines = [] + + for app in apps: + url = app["url"].rstrip("/") + branch = app["branch"] + name = url.split("/")[-1] + owner = url.split("/")[-2] + + tag = latest_tag(owner, name) + sha = branch_sha(owner, name, branch) + current = app.get("tag") or app.get("branch") + + status = "" + if tag and tag != app.get("tag"): + status = f"NEW TAG {tag}" + if "tag" not in app or app.get("tag") != tag: + app["_new_tag"] = tag + else: + status = f"up-to-date (tag={tag or 'none'}, sha={sha})" + + line = f" {name:20s} branch={branch:12s} {status}" + print(line) + report_lines.append(line) + + # Записываем отчёт для следующих шагов + report = "\n".join(report_lines) + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + f.write(f"report< Создано автоматически workflow `check-app-updates` + labels: | + dependencies + automated