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