mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-06-30 18:54:51 +02:00
ci: add CCMA release builds
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
name: PR Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
pr_check:
|
||||
name: Project PR check (linux-amd64)
|
||||
runs-on: linux-amd64
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
shell: bash
|
||||
env:
|
||||
CI_MATCHA_GITEA_TOKEN: ${{ secrets.CI_MATCHA_GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${CI_MATCHA_GITEA_TOKEN:-}" ]; then
|
||||
echo "Missing secrets.CI_MATCHA_GITEA_TOKEN" >&2
|
||||
exit 1
|
||||
fi
|
||||
basic=$(python3 -c 'import base64, os; print(base64.b64encode(("matcha:" + os.environ["CI_MATCHA_GITEA_TOKEN"]).encode()).decode())')
|
||||
git init .
|
||||
git remote add origin "https://git.hiabuto.net/${{ github.repository }}.git"
|
||||
git config --global http.https://git.hiabuto.net/.extraheader "AUTHORIZATION: basic ${basic}"
|
||||
git fetch --depth=1 origin "${{ github.event.pull_request.head.ref }}"
|
||||
git checkout --force FETCH_HEAD
|
||||
|
||||
- name: Load central CI templates
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git submodule sync --recursive
|
||||
git submodule update --init --remote ci-templates
|
||||
git -C ci-templates rev-parse --short HEAD
|
||||
|
||||
- name: Run config-driven PR check
|
||||
uses: ./ci-templates/actions/ci-runner
|
||||
with:
|
||||
command: pr-check
|
||||
config: ci-config.yaml
|
||||
@@ -0,0 +1,42 @@
|
||||
name: Push Smoke Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [dev, main]
|
||||
|
||||
jobs:
|
||||
smoke_build:
|
||||
name: Project smoke build (linux-amd64)
|
||||
runs-on: linux-amd64
|
||||
if: ${{ !contains(github.event.head_commit.message, '[nobuild]') }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
shell: bash
|
||||
env:
|
||||
CI_MATCHA_GITEA_TOKEN: ${{ secrets.CI_MATCHA_GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${CI_MATCHA_GITEA_TOKEN:-}" ]; then
|
||||
echo "Missing secrets.CI_MATCHA_GITEA_TOKEN" >&2
|
||||
exit 1
|
||||
fi
|
||||
basic=$(python3 -c 'import base64, os; print(base64.b64encode(("matcha:" + os.environ["CI_MATCHA_GITEA_TOKEN"]).encode()).decode())')
|
||||
git init .
|
||||
git remote add origin "https://git.hiabuto.net/${{ github.repository }}.git"
|
||||
git config --global http.https://git.hiabuto.net/.extraheader "AUTHORIZATION: basic ${basic}"
|
||||
git fetch --depth=1 origin "${{ github.ref_name }}"
|
||||
git checkout --force FETCH_HEAD
|
||||
|
||||
- name: Load central CI templates
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git submodule sync --recursive
|
||||
git submodule update --init --remote ci-templates
|
||||
git -C ci-templates rev-parse --short HEAD
|
||||
|
||||
- name: Run config-driven smoke build
|
||||
uses: ./ci-templates/actions/ci-runner
|
||||
with:
|
||||
command: smoke-build
|
||||
config: ci-config.yaml
|
||||
@@ -0,0 +1,106 @@
|
||||
name: Dev Version Bump
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Push Smoke Build"]
|
||||
types: [completed]
|
||||
branches: [dev]
|
||||
|
||||
env:
|
||||
CI_GIT_EMAIL: 'git-ci@hiabuto.net'
|
||||
CI_GIT_NAME: 'Git-CI'
|
||||
GITEA_REPO: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
runs-on: linux-amd64
|
||||
if: "${{ gitea.event.workflow_run.conclusion == 'success' && gitea.event.workflow_run.head_branch == 'dev' }}"
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
shell: bash
|
||||
env:
|
||||
CI_MATCHA_GITEA_TOKEN: ${{ secrets.CI_MATCHA_GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git init .
|
||||
git remote add origin "https://git.hiabuto.net/${{ github.repository }}.git"
|
||||
if [ -n "${CI_MATCHA_GITEA_TOKEN:-}" ]; then
|
||||
basic=$(python3 -c 'import base64, os; print(base64.b64encode(("matcha:" + os.environ["CI_MATCHA_GITEA_TOKEN"]).encode()).decode())')
|
||||
git config --global http.https://git.hiabuto.net/.extraheader "AUTHORIZATION: basic ${basic}"
|
||||
else
|
||||
echo "CI_MATCHA_GITEA_TOKEN not set; trying unauthenticated checkout."
|
||||
fi
|
||||
git fetch --depth=50 origin "${{ gitea.event.workflow_run.head_branch }}"
|
||||
git checkout --force "${{ gitea.event.workflow_run.head_sha }}"
|
||||
|
||||
- name: Load central CI templates
|
||||
shell: bash
|
||||
env:
|
||||
CI_MATCHA_GITEA_TOKEN: ${{ secrets.CI_MATCHA_GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${CI_MATCHA_GITEA_TOKEN:-}" ]; then
|
||||
echo "Missing secrets.CI_MATCHA_GITEA_TOKEN" >&2
|
||||
exit 1
|
||||
fi
|
||||
basic=$(python3 -c 'import base64, os; print(base64.b64encode(("matcha:" + os.environ["CI_MATCHA_GITEA_TOKEN"]).encode()).decode())')
|
||||
git config --global http.https://git.hiabuto.net/.extraheader "AUTHORIZATION: basic ${basic}"
|
||||
git submodule sync --recursive
|
||||
git submodule update --init --remote ci-templates
|
||||
git -C ci-templates rev-parse --short HEAD
|
||||
|
||||
- id: ver
|
||||
name: Compute version
|
||||
uses: ./ci-templates/.gitea/actions/actions-versioning
|
||||
with:
|
||||
config_file: ci-config.yaml
|
||||
ref_name: ${{ gitea.event.workflow_run.head_branch }}
|
||||
|
||||
- name: Commit + push VERSION (dev)
|
||||
shell: bash
|
||||
env:
|
||||
CI_MATCHA_GITEA_TOKEN: ${{ secrets.CI_MATCHA_GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${CI_MATCHA_GITEA_TOKEN:-}" ]; then
|
||||
echo "Missing secrets.CI_MATCHA_GITEA_TOKEN" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
subj=$(git log -1 --pretty=format:%s)
|
||||
if [[ "$subj" == *"[nobuild]"* ]]; then
|
||||
echo "[nobuild] present; skipping dev bump."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$subj" == ci:\ bump\ dev\ version* ]]; then
|
||||
echo "Already a dev bump commit; nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
version="${{ steps.ver.outputs.program_version }}"
|
||||
version_file=$(python3 - <<'PY'
|
||||
import re
|
||||
p='VERSION'
|
||||
for line in open('ci-config.yaml', encoding='utf-8'):
|
||||
if re.match(r'^\s*file\s*:', line):
|
||||
p=line.split(':',1)[1].strip().strip('"\'')
|
||||
break
|
||||
print(p)
|
||||
PY
|
||||
)
|
||||
|
||||
git config user.email "${CI_GIT_EMAIL}"
|
||||
git config user.name "${CI_GIT_NAME}"
|
||||
|
||||
echo "$version" > "$version_file"
|
||||
git add "$version_file"
|
||||
if git diff --cached --quiet; then
|
||||
echo "VERSION already up-to-date; nothing to commit."; exit 0
|
||||
fi
|
||||
|
||||
git commit -m "ci: bump dev version to ${version} [skip ci]"
|
||||
git config --local --unset-all http.https://git.hiabuto.net/.extraheader || true
|
||||
git config --global --unset-all http.https://git.hiabuto.net/.extraheader || true
|
||||
git remote set-url origin "https://matcha:${CI_MATCHA_GITEA_TOKEN}@git.hiabuto.net/${GITEA_REPO}.git"
|
||||
git push origin HEAD:dev
|
||||
@@ -0,0 +1,352 @@
|
||||
name: Main Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: release-main
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
CI_GIT_EMAIL: 'git-ci@hiabuto.net'
|
||||
CI_GIT_NAME: 'Git-CI'
|
||||
GITEA_API_BASE: 'https://git.hiabuto.net/api/v1'
|
||||
GITEA_REPO: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
compute:
|
||||
runs-on: linux-amd64
|
||||
if: ${{ github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' }}
|
||||
outputs:
|
||||
program_version: ${{ steps.ver.outputs.program_version }}
|
||||
did_release: ${{ steps.ver.outputs.did_release }}
|
||||
release_tag: ${{ steps.ver.outputs.release_tag }}
|
||||
release_kind: ${{ steps.ver.outputs.release_kind }}
|
||||
head_sha: ${{ steps.meta.outputs.head_sha }}
|
||||
head_msg: ${{ steps.meta.outputs.head_msg }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
shell: bash
|
||||
env:
|
||||
CI_MATCHA_GITEA_TOKEN: ${{ secrets.CI_MATCHA_GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git init .
|
||||
git remote add origin "https://git.hiabuto.net/${{ github.repository }}.git"
|
||||
if [ -n "${CI_MATCHA_GITEA_TOKEN:-}" ]; then
|
||||
basic=$(python3 -c 'import base64, os; print(base64.b64encode(("matcha:" + os.environ["CI_MATCHA_GITEA_TOKEN"]).encode()).decode())')
|
||||
git config --global http.https://git.hiabuto.net/.extraheader "AUTHORIZATION: basic ${basic}"
|
||||
else
|
||||
echo "CI_MATCHA_GITEA_TOKEN not set; trying unauthenticated checkout."
|
||||
fi
|
||||
git fetch --depth=50 origin "${{ github.ref_name }}"
|
||||
git checkout --force "${{ github.sha }}"
|
||||
|
||||
- name: Load central CI templates
|
||||
shell: bash
|
||||
env:
|
||||
CI_MATCHA_GITEA_TOKEN: ${{ secrets.CI_MATCHA_GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${CI_MATCHA_GITEA_TOKEN:-}" ]; then
|
||||
echo "Missing secrets.CI_MATCHA_GITEA_TOKEN" >&2
|
||||
exit 1
|
||||
fi
|
||||
basic=$(python3 -c 'import base64, os; print(base64.b64encode(("matcha:" + os.environ["CI_MATCHA_GITEA_TOKEN"]).encode()).decode())')
|
||||
git config --global http.https://git.hiabuto.net/.extraheader "AUTHORIZATION: basic ${basic}"
|
||||
git submodule sync --recursive
|
||||
git submodule update --init --remote ci-templates
|
||||
git -C ci-templates rev-parse --short HEAD
|
||||
|
||||
- id: meta
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "head_sha=${{ github.sha }}" >> "$GITHUB_OUTPUT"
|
||||
echo "head_msg=$(git log -1 --pretty=format:%s)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- id: ver
|
||||
name: Compute version
|
||||
uses: ./ci-templates/.gitea/actions/actions-versioning
|
||||
with:
|
||||
config_file: ci-config.yaml
|
||||
ref_name: ${{ github.ref_name }}
|
||||
|
||||
package:
|
||||
needs: [compute]
|
||||
if: ${{ needs.compute.outputs.did_release == 'true' && !contains(needs.compute.outputs.head_msg, '[nobuild]') }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os_label: linux-amd64
|
||||
os: linux
|
||||
arch: amd64
|
||||
- os_label: linux-arm64
|
||||
os: linux
|
||||
arch: arm64
|
||||
- os_label: windows-amd64
|
||||
os: windows
|
||||
arch: amd64
|
||||
runs-on: ${{ matrix.os_label }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
shell: bash
|
||||
env:
|
||||
CI_MATCHA_GITEA_TOKEN: ${{ secrets.CI_MATCHA_GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git init .
|
||||
git remote add origin "https://git.hiabuto.net/${{ github.repository }}.git"
|
||||
if [ -n "${CI_MATCHA_GITEA_TOKEN:-}" ]; then
|
||||
basic=$(python3 -c 'import base64, os; print(base64.b64encode(("matcha:" + os.environ["CI_MATCHA_GITEA_TOKEN"]).encode()).decode())')
|
||||
git config --global http.https://git.hiabuto.net/.extraheader "AUTHORIZATION: basic ${basic}"
|
||||
else
|
||||
echo "CI_MATCHA_GITEA_TOKEN not set; trying unauthenticated checkout."
|
||||
fi
|
||||
git fetch --depth=50 origin "${{ github.ref_name }}"
|
||||
git checkout --force "${{ needs.compute.outputs.head_sha }}"
|
||||
|
||||
- name: Load central CI templates
|
||||
shell: bash
|
||||
env:
|
||||
CI_MATCHA_GITEA_TOKEN: ${{ secrets.CI_MATCHA_GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${CI_MATCHA_GITEA_TOKEN:-}" ]; then
|
||||
echo "Missing secrets.CI_MATCHA_GITEA_TOKEN" >&2
|
||||
exit 1
|
||||
fi
|
||||
basic=$(python3 -c 'import base64, os; print(base64.b64encode(("matcha:" + os.environ["CI_MATCHA_GITEA_TOKEN"]).encode()).decode())')
|
||||
git config --global http.https://git.hiabuto.net/.extraheader "AUTHORIZATION: basic ${basic}"
|
||||
git submodule sync --recursive
|
||||
git submodule update --init --remote ci-templates
|
||||
git -C ci-templates rev-parse --short HEAD
|
||||
|
||||
- name: Package release via central CI templates
|
||||
uses: ./ci-templates/actions/ci-runner
|
||||
env:
|
||||
CI_ARCH: ${{ matrix.arch }}
|
||||
with:
|
||||
command: package-release
|
||||
config: ci-config.yaml
|
||||
version: ${{ needs.compute.outputs.program_version }}
|
||||
|
||||
- name: Upload dist
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: dist-${{ matrix.os }}-${{ matrix.arch }}
|
||||
path: dist/
|
||||
|
||||
finalize_and_publish:
|
||||
needs: [compute, package]
|
||||
if: ${{ needs.compute.outputs.did_release == 'true' && needs.package.result == 'success' }}
|
||||
runs-on: linux-amd64
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
shell: bash
|
||||
env:
|
||||
CI_MATCHA_GITEA_TOKEN: ${{ secrets.CI_MATCHA_GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git init .
|
||||
git remote add origin "https://git.hiabuto.net/${{ github.repository }}.git"
|
||||
if [ -n "${CI_MATCHA_GITEA_TOKEN:-}" ]; then
|
||||
basic=$(python3 -c 'import base64, os; print(base64.b64encode(("matcha:" + os.environ["CI_MATCHA_GITEA_TOKEN"]).encode()).decode())')
|
||||
git config --global http.https://git.hiabuto.net/.extraheader "AUTHORIZATION: basic ${basic}"
|
||||
else
|
||||
echo "CI_MATCHA_GITEA_TOKEN not set; trying unauthenticated checkout."
|
||||
fi
|
||||
git fetch --depth=50 origin "${{ github.ref_name }}"
|
||||
git checkout --force "${{ needs.compute.outputs.head_sha }}"
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: release-artifacts
|
||||
|
||||
- name: Prepare upload dir
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p upload
|
||||
find release-artifacts -maxdepth 4 -type f -print -exec cp -f {} upload/ \;
|
||||
ls -la upload
|
||||
|
||||
- name: Commit VERSION + tag + push (only now)
|
||||
shell: bash
|
||||
env:
|
||||
CI_MATCHA_GITEA_TOKEN: ${{ secrets.CI_MATCHA_GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${CI_MATCHA_GITEA_TOKEN:-}" ]; then
|
||||
echo "Missing secrets.CI_MATCHA_GITEA_TOKEN" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
version="${{ needs.compute.outputs.program_version }}"
|
||||
tag="${{ needs.compute.outputs.release_tag }}"
|
||||
version_file=$(python3 - <<'PY'
|
||||
import re
|
||||
p='VERSION'
|
||||
for line in open('ci-config.yaml', encoding='utf-8'):
|
||||
if re.match(r'^\s*file\s*:', line):
|
||||
p=line.split(':',1)[1].strip().strip('"\'')
|
||||
break
|
||||
print(p)
|
||||
PY
|
||||
)
|
||||
|
||||
git config user.email "${CI_GIT_EMAIL}"
|
||||
git config user.name "${CI_GIT_NAME}"
|
||||
|
||||
echo "$version" > "$version_file"
|
||||
git add "$version_file"
|
||||
if git diff --cached --quiet; then
|
||||
echo "VERSION already set in repo; continuing"
|
||||
else
|
||||
git commit -m "ci: release version ${version} [skip ci]"
|
||||
fi
|
||||
|
||||
git config --local --unset-all http.https://git.hiabuto.net/.extraheader || true
|
||||
git config --global --unset-all http.https://git.hiabuto.net/.extraheader || true
|
||||
git remote set-url origin "https://matcha:${CI_MATCHA_GITEA_TOKEN}@git.hiabuto.net/${GITEA_REPO}.git"
|
||||
git fetch --tags origin
|
||||
git push origin "HEAD:main"
|
||||
|
||||
if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
|
||||
echo "Tag already exists on origin: ${tag} (will reuse)"
|
||||
else
|
||||
git tag "${tag}"
|
||||
git push origin "${tag}"
|
||||
fi
|
||||
|
||||
- name: Publish release + upload assets
|
||||
shell: bash
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.CI_MATCHA_GITEA_TOKEN }}
|
||||
TAG: ${{ needs.compute.outputs.release_tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${GITEA_TOKEN:-}" ]; then
|
||||
echo "Missing secrets.CI_MATCHA_GITEA_TOKEN" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 - <<'PY'
|
||||
import json, os, pathlib, urllib.parse, urllib.request, urllib.error
|
||||
api_base = os.environ['GITEA_API_BASE'].rstrip('/')
|
||||
repo = os.environ['GITEA_REPO']
|
||||
token = os.environ['GITEA_TOKEN'].strip()
|
||||
tag = os.environ['TAG'].strip()
|
||||
artifacts = sorted(p for p in pathlib.Path('upload').glob('*') if p.is_file())
|
||||
headers = {'Authorization': f'token {token}', 'Accept': 'application/json'}
|
||||
def request_json(method, url, data=None):
|
||||
payload=None
|
||||
h=dict(headers)
|
||||
if data is not None:
|
||||
payload=json.dumps(data).encode('utf-8'); h['Content-Type']='application/json'
|
||||
req=urllib.request.Request(url, data=payload, method=method, headers=h)
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
txt=resp.read().decode('utf-8'); return resp.getcode(), json.loads(txt) if txt else None
|
||||
except urllib.error.HTTPError as exc:
|
||||
return exc.code, {'error': exc.read().decode('utf-8', errors='replace')}
|
||||
def request_binary_post(url, payload, content_type):
|
||||
h=dict(headers); h['Content-Type']=content_type
|
||||
req=urllib.request.Request(url, data=payload, method='POST', headers=h)
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
txt=resp.read().decode('utf-8'); return resp.getcode(), json.loads(txt) if txt else None
|
||||
tag_url=f"{api_base}/repos/{repo}/releases/tags/{urllib.parse.quote(tag, safe='')}"
|
||||
status, release = request_json('GET', tag_url)
|
||||
if status==200 and isinstance(release, dict):
|
||||
release_id=release.get('id')
|
||||
else:
|
||||
status, release = request_json('POST', f"{api_base}/repos/{repo}/releases", {
|
||||
'tag_name': tag, 'name': tag, 'body': f'Automated release for {tag}', 'draft': False, 'prerelease': False,
|
||||
})
|
||||
if status not in (200,201):
|
||||
raise SystemExit(f"create release failed {status} {release}")
|
||||
release_id=release.get('id')
|
||||
assets_url=f"{api_base}/repos/{repo}/releases/{release_id}/assets"
|
||||
status, existing = request_json('GET', assets_url)
|
||||
existing = existing if isinstance(existing, list) else []
|
||||
by_name={e.get('name'):e for e in existing if isinstance(e, dict) and e.get('name')}
|
||||
for a in artifacts:
|
||||
name=a.name
|
||||
prev=by_name.get(name)
|
||||
if prev and prev.get('id'):
|
||||
ds,_=request_json('DELETE', f"{assets_url}/{prev['id']}")
|
||||
if ds!=204: raise SystemExit(f"delete asset failed {name} {ds}")
|
||||
code,_=request_binary_post(f"{assets_url}?name={urllib.parse.quote(name, safe='')}", a.read_bytes(), 'application/octet-stream')
|
||||
if code not in (200,201): raise SystemExit(f"upload failed {name} {code}")
|
||||
print('Uploaded', name)
|
||||
print('Release done', tag)
|
||||
PY
|
||||
|
||||
sync_main_to_dev:
|
||||
needs: [finalize_and_publish]
|
||||
if: ${{ needs.finalize_and_publish.result == 'success' }}
|
||||
runs-on: linux-amd64
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
shell: bash
|
||||
env:
|
||||
CI_MATCHA_GITEA_TOKEN: ${{ secrets.CI_MATCHA_GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git init .
|
||||
git remote add origin "https://git.hiabuto.net/${{ github.repository }}.git"
|
||||
if [ -n "${CI_MATCHA_GITEA_TOKEN:-}" ]; then
|
||||
basic=$(python3 -c 'import base64, os; print(base64.b64encode(("matcha:" + os.environ["CI_MATCHA_GITEA_TOKEN"]).encode()).decode())')
|
||||
git config --global http.https://git.hiabuto.net/.extraheader "AUTHORIZATION: basic ${basic}"
|
||||
else
|
||||
echo "CI_MATCHA_GITEA_TOKEN not set; trying unauthenticated checkout."
|
||||
fi
|
||||
git fetch --depth=50 origin "${{ github.ref_name }}"
|
||||
git checkout --force "${{ github.sha }}"
|
||||
|
||||
- name: Merge main into dev + bump dev0
|
||||
shell: bash
|
||||
env:
|
||||
CI_MATCHA_GITEA_TOKEN: ${{ secrets.CI_MATCHA_GITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${CI_MATCHA_GITEA_TOKEN:-}" ]; then
|
||||
echo "Missing secrets.CI_MATCHA_GITEA_TOKEN" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git config user.email "${CI_GIT_EMAIL}"
|
||||
git config user.name "${CI_GIT_NAME}"
|
||||
|
||||
git config --local --unset-all http.https://git.hiabuto.net/.extraheader || true
|
||||
git config --global --unset-all http.https://git.hiabuto.net/.extraheader || true
|
||||
git remote set-url origin "https://matcha:${CI_MATCHA_GITEA_TOKEN}@git.hiabuto.net/${GITEA_REPO}.git"
|
||||
git fetch origin main dev
|
||||
git checkout -B dev origin/dev
|
||||
git merge origin/main -m "ci: merge main into dev [skip ci]"
|
||||
|
||||
version_file=$(python3 - <<'PY'
|
||||
import re
|
||||
p='VERSION'
|
||||
for line in open('ci-config.yaml', encoding='utf-8'):
|
||||
if re.match(r'^\s*file\s*:', line):
|
||||
p=line.split(':',1)[1].strip().strip('"\'')
|
||||
break
|
||||
print(p)
|
||||
PY
|
||||
)
|
||||
main_version=$(git show origin/main:"$version_file" | head -n1 | tr -d '\r\n' | xargs)
|
||||
python3 -c "import re,sys; v=sys.argv[1].strip(); m=re.search(r'(\\d+)\\.(\\d+)\\.(\\d+)', v); (m is not None) or (_ for _ in ()).throw(SystemExit(f'Bad VERSION on main: {v!r}')); print(f'{m.group(1)}.{m.group(2)}.{m.group(3)}-dev0')" "$main_version" > "$version_file"
|
||||
|
||||
git add "$version_file"
|
||||
if git diff --cached --quiet; then
|
||||
echo "No VERSION changes after sync.";
|
||||
else
|
||||
git commit -m "ci: bump dev version to $(cat "$version_file") [skip ci]"
|
||||
fi
|
||||
|
||||
git push origin HEAD:dev
|
||||
@@ -5,6 +5,11 @@ __pycache__/
|
||||
.ruff_cache/
|
||||
.venv/
|
||||
build/
|
||||
!build/
|
||||
build/*
|
||||
!build/ccma.spec
|
||||
!build/build.ps1
|
||||
!build/build.bat
|
||||
dist/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
[submodule "ci-templates"]
|
||||
path = ci-templates
|
||||
url = https://git.hiabuto.net/CI-Build/ci-templates.git
|
||||
branch = main
|
||||
update = checkout
|
||||
@@ -59,8 +59,8 @@ also checks the default install locations under `%PROGRAMFILES%` and
|
||||
|
||||
## Building standalone executables
|
||||
|
||||
To bundle CCMA into a single-file Windows executable with PyInstaller, use the
|
||||
build script in the `build/` directory:
|
||||
CCMA uses the same PyInstaller-based release flow locally and in CI. The
|
||||
single-file executables are built from `build/ccma.spec`.
|
||||
|
||||
### Windows standalone build
|
||||
|
||||
@@ -76,9 +76,30 @@ This creates two executables in the `dist/` directory:
|
||||
The build script automatically:
|
||||
- Creates a venv if missing
|
||||
- Installs dependencies and PyInstaller
|
||||
- Generates a Windows icon from the splash image
|
||||
- Generates a Windows icon from the CCMA icon
|
||||
- Runs PyInstaller to bundle the application
|
||||
|
||||
### Linux standalone build
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
. .venv/bin/activate
|
||||
python -m pip install -U pip
|
||||
pip install -r requirements.txt
|
||||
pip install pyinstaller
|
||||
python -m PyInstaller --noconfirm --clean build/ccma.spec
|
||||
```
|
||||
|
||||
This creates `dist/ccma`. In CI, release builds additionally rename artifacts
|
||||
to `ccma-<version>-linux-<arch>` and `ccma-<version>-windows-<arch>.exe`.
|
||||
|
||||
### CI release builds
|
||||
|
||||
The Gitea workflows use the central `ci-templates` submodule and
|
||||
`ci-config.yaml` to run PR checks, Linux smoke builds, and release packaging for
|
||||
Linux amd64, Linux arm64, and Windows amd64. A release build is skipped when the
|
||||
commit message contains `[nobuild]`.
|
||||
|
||||
On first start, select or create the central member-store directory. The
|
||||
`VERSION` file is the single source for application and package versions.
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
set SCRIPT_DIR=%~dp0
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%build.ps1"
|
||||
|
||||
if errorlevel 1 (
|
||||
echo.
|
||||
echo Build failed.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Build finished.
|
||||
pause
|
||||
@@ -0,0 +1,98 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$NoClean
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$repoRoot = Resolve-Path (Join-Path $scriptDir '..')
|
||||
Set-Location $repoRoot
|
||||
|
||||
function Get-VersionString {
|
||||
$versionFile = Join-Path $repoRoot 'VERSION'
|
||||
if (!(Test-Path $versionFile)) { return 'unknown' }
|
||||
return (Get-Content $versionFile -Raw).Trim()
|
||||
}
|
||||
|
||||
function Sanitize-FilePart([string]$text) {
|
||||
if ([string]::IsNullOrWhiteSpace($text)) { return 'unknown' }
|
||||
$s = $text.Trim()
|
||||
foreach ($ch in @('\','/','?',':','*','"','<','>','|')) {
|
||||
$s = $s.Replace($ch, '_')
|
||||
}
|
||||
$s = $s -replace '\s+', '_'
|
||||
return $s
|
||||
}
|
||||
|
||||
function Get-PlatformToken {
|
||||
return "windows"
|
||||
}
|
||||
|
||||
function Get-ArchToken {
|
||||
switch ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant()) {
|
||||
"x64" { return "amd64" }
|
||||
"arm64" { return "arm64" }
|
||||
default { return ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant()) }
|
||||
}
|
||||
}
|
||||
|
||||
$python = Join-Path $repoRoot '.venv\Scripts\python.exe'
|
||||
if (!(Test-Path $python)) {
|
||||
Write-Host "No .venv found. Creating venv..." -ForegroundColor Yellow
|
||||
python -m venv .venv
|
||||
if (!(Test-Path $python)) {
|
||||
throw "Failed to create venv; expected at $python"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Using Python: $python" -ForegroundColor Cyan
|
||||
|
||||
& $python -m pip install -r requirements.txt
|
||||
& $python -m pip install pyinstaller pillow
|
||||
|
||||
$pngIcon = Join-Path $repoRoot 'src\ccma\assets\icons\ccma.png'
|
||||
$icoOut = Join-Path $repoRoot 'build\app.ico'
|
||||
if (Test-Path $pngIcon) {
|
||||
Write-Host "Generating ICO: $icoOut" -ForegroundColor Cyan
|
||||
& $python -c @"
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
png = Path(r'$pngIcon')
|
||||
ico = Path(r'$icoOut')
|
||||
ico.parent.mkdir(parents=True, exist_ok=True)
|
||||
img = Image.open(png).convert('RGBA')
|
||||
img.save(ico, format='ICO', sizes=[(16,16),(24,24),(32,32),(48,48),(64,64),(128,128),(256,256)])
|
||||
print(str(ico))
|
||||
"@
|
||||
} else {
|
||||
Write-Host "PNG icon not found at: $pngIcon (building without icon)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
if (-not $NoClean) {
|
||||
if (Test-Path (Join-Path $repoRoot 'dist')) { Remove-Item -Recurse -Force (Join-Path $repoRoot 'dist') }
|
||||
if (Test-Path (Join-Path $repoRoot 'build\pyinstaller')) { Remove-Item -Recurse -Force (Join-Path $repoRoot 'build\pyinstaller') }
|
||||
}
|
||||
|
||||
Write-Host "Building with PyInstaller spec..." -ForegroundColor Cyan
|
||||
& $python -m PyInstaller --noconfirm --clean --distpath dist --workpath build\pyinstaller build\ccma.spec
|
||||
|
||||
$exe = Join-Path $repoRoot 'dist\ccma.exe'
|
||||
if (!(Test-Path $exe)) {
|
||||
throw "Build finished but exe not found at: $exe"
|
||||
}
|
||||
|
||||
$version = Get-VersionString
|
||||
$versionSafe = Sanitize-FilePart $version
|
||||
$platformToken = Get-PlatformToken
|
||||
$archToken = Get-ArchToken
|
||||
$versionedExe = Join-Path $repoRoot ("dist\ccma-$versionSafe-$platformToken-$archToken.exe")
|
||||
|
||||
Copy-Item -Force $exe $versionedExe
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Build OK" -ForegroundColor Green
|
||||
Write-Host "Base EXE: $exe" -ForegroundColor Green
|
||||
Write-Host "Versioned EXE: $versionedExe" -ForegroundColor Green
|
||||
Write-Host "Version: $version" -ForegroundColor Green
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
"""PyInstaller spec for CCMA.
|
||||
|
||||
Packaged output:
|
||||
- Windows: dist/ccma.exe
|
||||
- Linux: dist/ccma
|
||||
|
||||
Icon:
|
||||
- On Windows the spec auto-generates build/app.ico from the PNG icon if it does
|
||||
not already exist, so direct PyInstaller and scripted builds behave the same.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from PyInstaller.utils.hooks import collect_data_files
|
||||
|
||||
block_cipher = None
|
||||
|
||||
spec_dir = Path(globals().get("SPECPATH", Path.cwd())).resolve()
|
||||
project_root = spec_dir.parent
|
||||
entry_script = project_root / "main.py"
|
||||
package_src = project_root / "src" / "ccma"
|
||||
assets_src = package_src / "assets"
|
||||
app_name_slug = "ccma"
|
||||
|
||||
win_icon_path = project_root / "build" / "app.ico"
|
||||
png_icon_path = assets_src / "icons" / "ccma.png"
|
||||
|
||||
if sys.platform == "win32" and not win_icon_path.exists() and png_icon_path.exists():
|
||||
try:
|
||||
from PIL import Image
|
||||
|
||||
img = Image.open(png_icon_path).convert("RGBA")
|
||||
win_icon_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
img.save(
|
||||
win_icon_path,
|
||||
format="ICO",
|
||||
sizes=[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
|
||||
)
|
||||
print(f"[spec] Generated ICO: {win_icon_path}")
|
||||
except Exception as exc:
|
||||
print(f"[spec] WARNING: ICO generation failed: {exc}")
|
||||
|
||||
win_icon = str(win_icon_path) if win_icon_path.exists() else None
|
||||
|
||||
datas: list[tuple[str, str]] = []
|
||||
if assets_src.exists():
|
||||
datas.append((str(assets_src), "ccma/assets"))
|
||||
|
||||
version_file = project_root / "VERSION"
|
||||
if version_file.exists():
|
||||
datas.append((str(version_file), "."))
|
||||
datas.append((str(version_file), "ccma"))
|
||||
|
||||
for optional_pkg in ("ttkbootstrap_icons", "ttkbootstrap_icons_mat"):
|
||||
try:
|
||||
datas += collect_data_files(optional_pkg)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
hiddenimports = ["ttkbootstrap_icons", "ttkbootstrap_icons_mat"]
|
||||
|
||||
a = Analysis(
|
||||
[str(entry_script)],
|
||||
pathex=[str(project_root), str(project_root / "src")],
|
||||
binaries=[],
|
||||
datas=datas,
|
||||
hiddenimports=hiddenimports,
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name=app_name_slug,
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon=win_icon,
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
# Repo-spezifische CI-Konfiguration fuer CI-Build/ci-templates.
|
||||
branches:
|
||||
dev: dev
|
||||
main: main
|
||||
|
||||
project:
|
||||
type: python
|
||||
|
||||
version:
|
||||
type: file
|
||||
file: VERSION
|
||||
|
||||
changelog:
|
||||
path: src/ccma/assets/CHANGELOG.json
|
||||
required_for_dev_pr: true
|
||||
no_changelog_markers: ["[no changelog]", "no changelog"]
|
||||
|
||||
python:
|
||||
binary: python3
|
||||
venv: .venv
|
||||
requirements: requirements.txt
|
||||
compile_path: src/ccma
|
||||
test_paths: ["tests"]
|
||||
check:
|
||||
ruff: true
|
||||
pytest: true
|
||||
extra_pip: ["ruff", "pytest"]
|
||||
|
||||
package:
|
||||
type: pyinstaller
|
||||
pyinstaller:
|
||||
spec: build/ccma.spec
|
||||
arch: amd64
|
||||
artifacts:
|
||||
linux:
|
||||
source: dist/ccma
|
||||
rename: ccma-{version}-linux-{arch}
|
||||
windows:
|
||||
source: dist/ccma.exe
|
||||
rename: ccma-{version}-windows-{arch}.exe
|
||||
|
||||
release:
|
||||
patch_markers: ["[patch]", "[hotfix]"]
|
||||
Submodule
+1
Submodule ci-templates added at 6891ac36d4
@@ -53,6 +53,7 @@ pythonpath = ["src"]
|
||||
[tool.ruff]
|
||||
line-length = 110
|
||||
target-version = "py311"
|
||||
extend-exclude = ["ci-templates"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "UP", "B"]
|
||||
|
||||
+2
-12
@@ -71,7 +71,7 @@ class AppConfig:
|
||||
|
||||
|
||||
def config_directory() -> Path:
|
||||
override = os.environ.get("CCMA_CONFIG_DIR") or os.environ.get("C3MA_CONFIG_DIR")
|
||||
override = os.environ.get("CCMA_CONFIG_DIR")
|
||||
if override:
|
||||
return Path(override).expanduser()
|
||||
if os.name == "nt":
|
||||
@@ -81,11 +81,7 @@ def config_directory() -> Path:
|
||||
|
||||
def load_config() -> AppConfig:
|
||||
path = config_directory() / "config.json"
|
||||
if not path.exists() and not (os.environ.get("CCMA_CONFIG_DIR") or os.environ.get("C3MA_CONFIG_DIR")):
|
||||
legacy_path = _legacy_config_directory() / "config.json"
|
||||
if legacy_path.exists():
|
||||
path = legacy_path
|
||||
store_override = os.environ.get("CCMA_STORE") or os.environ.get("C3MA_STORE", "")
|
||||
store_override = os.environ.get("CCMA_STORE", "")
|
||||
if not path.exists():
|
||||
return AppConfig(store_path=store_override)
|
||||
try:
|
||||
@@ -113,12 +109,6 @@ def load_config() -> AppConfig:
|
||||
return AppConfig(store_path=store_override)
|
||||
|
||||
|
||||
def _legacy_config_directory() -> Path:
|
||||
if os.name == "nt":
|
||||
return Path(os.environ.get("APPDATA", Path.home())) / "C3MA"
|
||||
return Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "c3ma"
|
||||
|
||||
|
||||
def _non_negative_float(value: object, default: float) -> float:
|
||||
try:
|
||||
parsed = float(value)
|
||||
|
||||
@@ -511,9 +511,9 @@ def _office_executable() -> str | None:
|
||||
if value and value not in windows_roots:
|
||||
windows_roots.append(value)
|
||||
for root in windows_roots:
|
||||
candidate = Path(root) / "LibreOffice" / "program" / "soffice.exe"
|
||||
if candidate.is_file():
|
||||
return str(candidate)
|
||||
candidate = os.path.join(root, "LibreOffice", "program", "soffice.exe")
|
||||
if os.path.isfile(candidate):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -39,10 +39,8 @@ def test_splash_minimum_defaults_to_five_and_is_clamped(tmp_path, monkeypatch) -
|
||||
assert load_config().splash_minimum_seconds == 0.0
|
||||
|
||||
|
||||
def test_legacy_c3ma_environment_variables_are_still_read(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.delenv("CCMA_CONFIG_DIR", raising=False)
|
||||
monkeypatch.delenv("CCMA_STORE", raising=False)
|
||||
monkeypatch.setenv("C3MA_CONFIG_DIR", str(tmp_path / "legacy-config"))
|
||||
monkeypatch.setenv("C3MA_STORE", str(tmp_path / "legacy-store"))
|
||||
def test_store_path_can_be_overridden_from_ccma_environment(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setenv("CCMA_CONFIG_DIR", str(tmp_path / "config"))
|
||||
monkeypatch.setenv("CCMA_STORE", str(tmp_path / "store"))
|
||||
|
||||
assert load_config().store_path == str(tmp_path / "legacy-store")
|
||||
assert load_config().store_path == str(tmp_path / "store")
|
||||
|
||||
Reference in New Issue
Block a user