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 <noreply@anthropic.com>
This commit is contained in:
abounoone 2026-03-18 09:07:19 +00:00
parent 717d19f966
commit 52337216f0
3 changed files with 264 additions and 0 deletions

40
.github/workflows/README.md vendored Normal file
View file

@ -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://<NEW_TOKEN>@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
```

92
.github/workflows/build-image.yml vendored Normal file
View file

@ -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

132
.github/workflows/check-app-updates.yml vendored Normal file
View file

@ -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<<EOF\n{report}\nEOF\n")
# Проверяем есть ли новые теги
has_updates = any("_new_tag" in a for a in apps)
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
f.write(f"has_updates={'true' if has_updates else 'false'}\n")
# Обновляем apps.json если есть новые теги
if has_updates:
for app in apps:
if "_new_tag" in app:
app["branch"] = app.pop("_new_tag")
app.pop("_new_tag", None)
with open("apps.json", "w") as f:
json.dump(apps, f, indent=2)
print("\n✓ apps.json обновлён")
else:
print("\n✓ Все приложения актуальны")
PYEOF
- name: Show update report
run: |
echo "=== Статус приложений ==="
echo "${{ steps.check.outputs.report }}"
- name: Create Pull Request with updates
if: steps.check.outputs.has_updates == 'true'
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "chore: update app versions in apps.json"
branch: auto/update-apps
delete-branch: true
title: "🔄 Обновление версий приложений Frappe"
body: |
Автоматическое обновление версий приложений.
**Изменения в apps.json:**
```
${{ steps.check.outputs.report }}
```
После merge необходимо:
1. `make build` — пересобрать образ
2. `make update` — задеплоить с миграциями
> Создано автоматически workflow `check-app-updates`
labels: |
dependencies
automated