ci: replace GitHub workflows with Gitea release workflow
Release / release (push) Successful in 9s

The three .github/workflows files (release.yml, compat-daily.yml,
cache-cleanup-weekly.yml) all target the GitHub API:
  - release.yml uses `gh release create`
  - compat-daily.yml does `git push https://x-access-token@github.com/...`
    to a `badges` branch
  - cache-cleanup-weekly.yml uses `gh cache delete`
On Gitea Actions these silently mis-route to GitHub with a Gitea-issued
GITHUB_TOKEN, which github.com rejects ("Invalid username or token") —
so every scheduled run fails.

Drop them on secure-main (upstream main keeps them for the original
GitHub repo, untouched) and add .gitea/workflows/release.yml that:
  - triggers on tag push v* or manual workflow_dispatch
  - builds release notes from the changelog window since the prior tag
  - creates the release via Gitea API, or refreshes assets if it already
    exists (so re-runs are idempotent)
  - attaches install.sh + install.ps1 so the README's
    /releases/latest/download/install.{sh,ps1} URLs resolve

Tag a release with:
  git tag v1.0.0 && git push origin v1.0.0
This commit is contained in:
omar
2026-05-21 03:34:39 +03:00
parent f6ee715478
commit 32663a7b18
4 changed files with 117 additions and 350 deletions
+117
View File
@@ -0,0 +1,117 @@
name: Release
# Triggered when a tag matching v* is pushed. Builds a Gitea release with
# install.sh + install.ps1 attached as assets, so README's
# `releases/latest/download/install.sh` URL resolves.
#
# Manual trigger also supported: workflow_dispatch with a tag input lets
# you re-publish assets onto an existing tag (idempotent — existing
# attachments are replaced).
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Tag to publish (e.g. v1.0.0). Tag must already exist.'
required: true
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Resolve tag
id: tag
run: |
if [ -n "${{ inputs.tag }}" ]; then
tag="${{ inputs.tag }}"
else
tag="${GITHUB_REF_NAME}"
fi
echo "name=${tag}" >> "$GITHUB_OUTPUT"
prev=$(git tag --sort=-version:refname | grep -v "^${tag}$" | head -n1 || true)
echo "prev=${prev}" >> "$GITHUB_OUTPUT"
- name: Build release notes
id: notes
run: |
tag="${{ steps.tag.outputs.name }}"
prev="${{ steps.tag.outputs.prev }}"
{
echo "## Changes"
echo
if [ -n "$prev" ]; then
git log --pretty='- %s (%h)' "${prev}..${tag}"
else
git log --pretty='- %s (%h)' "$tag"
fi
echo
echo "## Install"
echo
echo '**macOS / Linux:**'
echo '```bash'
echo "curl -fsSL ${{ github.server_url }}/${{ github.repository }}/releases/latest/download/install.sh | bash"
echo '```'
echo
echo '**Windows (PowerShell):**'
echo '```powershell'
echo "irm ${{ github.server_url }}/${{ github.repository }}/releases/latest/download/install.ps1 | iex"
echo '```'
} > release-notes.md
- name: Publish release with assets
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
SERVER: ${{ github.server_url }}
REPO: ${{ github.repository }}
TAG: ${{ steps.tag.outputs.name }}
run: |
set -euo pipefail
api="${SERVER}/api/v1/repos/${REPO}"
notes=$(cat release-notes.md)
payload=$(jq -n --arg tag "$TAG" --arg notes "$notes" \
'{tag_name: $tag, name: ("ClawGod " + $tag), body: $notes, draft: false, prerelease: false}')
# Find or create the release for this tag.
existing=$(curl -fsSL -H "Authorization: token $TOKEN" \
"${api}/releases/tags/${TAG}" 2>/dev/null || echo "")
rel_id=$(echo "$existing" | jq -r '.id // empty')
if [ -z "$rel_id" ]; then
rel=$(curl -fsSL -X POST \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "$payload" \
"${api}/releases")
rel_id=$(echo "$rel" | jq -r '.id')
echo "Created release $TAG (id=$rel_id)"
else
echo "Release $TAG already exists (id=$rel_id) — refreshing assets only"
fi
# Delete any pre-existing attachments with our names, then upload fresh.
assets=$(curl -fsSL -H "Authorization: token $TOKEN" \
"${api}/releases/${rel_id}/assets")
for name in install.sh install.ps1; do
old_id=$(echo "$assets" | jq -r --arg n "$name" '.[] | select(.name==$n) | .id' | head -n1)
if [ -n "$old_id" ]; then
curl -fsSL -X DELETE \
-H "Authorization: token $TOKEN" \
"${api}/releases/${rel_id}/assets/${old_id}"
echo "Deleted stale asset $name (id=$old_id)"
fi
curl -fsSL -X POST \
-H "Authorization: token $TOKEN" \
-H "Content-Type: multipart/form-data" \
-F "attachment=@${name}" \
"${api}/releases/${rel_id}/assets?name=${name}" >/dev/null
echo "Uploaded $name"
done
@@ -1,54 +0,0 @@
name: Cache Cleanup Weekly
# compat-daily.yml writes a fresh `~/.npm` cache entry on every successful
# run (key includes ${{ github.run_id }}, which is unique per run) and reads
# previous entries via restore-keys. GitHub's automatic 7-day cache expiry
# only kicks in for entries that haven't been *accessed* — the restore-keys
# match counts as access, so daily-tested entries never expire on their
# own. Without housekeeping, cache entries accumulate until we hit the
# 10 GB per-repo limit and GitHub starts evicting LRU, which sometimes
# kicks the entries we actually wanted to keep.
#
# Run weekly: list every cache, delete it. Next compat-daily run repopulates
# `~/.npm` from a fresh npm-registry fetch (one extra ~80 MB download per
# week, trivial cost).
on:
schedule:
- cron: '0 4 * * 0' # Sunday 04:00 UTC
workflow_dispatch:
permissions:
actions: write # required for `gh cache delete`
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
purge:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Delete all repo caches
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
shell: bash
run: |
set -euo pipefail
# gh cache list paginates; --limit 200 handles up to 200 entries
# in a single call (we'd never expect that many anyway).
ids=$(gh cache list --repo "$REPO" --limit 200 --json id --jq '.[].id')
if [[ -z "$ids" ]]; then
echo "No caches to delete."
exit 0
fi
count=$(printf '%s\n' "$ids" | grep -c . || true)
echo "Deleting $count cache entries…"
while IFS= read -r id; do
[[ -z "$id" ]] && continue
gh cache delete "$id" --repo "$REPO" \
&& echo " ✓ $id" \
|| echo " ✗ $id (already gone or transient error)"
done <<< "$ids"
echo "Done. Next compat-daily run will repopulate ~/.npm fresh."
-212
View File
@@ -1,212 +0,0 @@
name: Compat Daily
# Runs install.sh end-to-end on every supported platform against the current
# latest Claude Code from npm. Catches the kind of regression we hit when
# Anthropic bumps the embedded Bun runtime ahead of Bun's stable release.
on:
schedule:
- cron: '17 7 * * *' # ~07:17 UTC daily
workflow_dispatch:
push:
branches: [main]
paths:
- install.sh
- .github/workflows/compat-daily.yml
pull_request:
paths:
- install.sh
- .github/workflows/compat-daily.yml
permissions:
contents: write # write needed to push the version-badge JSON to `badges` branch
issues: write
# Opt every JS-based action (e.g. actions/checkout, actions/cache,
# actions/github-script) onto Node 24, ahead of GitHub forcing it on
# 2026-06-02. Silences the deprecation warning and keeps us aligned
# with the Node version we use to run patch.mjs / extract-natives.mjs.
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
smoke:
# Single Linux runner — a Linux failure almost always means cross-platform
# breakage upstream. macOS / Windows runners are 10x / 2x more expensive
# than Linux on shared-tier accounting, so we don't burn them on a daily
# signal.
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Set up Node.js 24
uses: actions/setup-node@v4
with:
node-version: 24
- name: Set up Bun (canary)
# Anthropic ships claude-code with bleeding-edge Bun (e.g. 1.3.14
# before it leaves the canary channel). Stable Bun panics with
# "Expected CommonJS module to have a function wrapper" when loading
# cli.original.cjs extracted from a newer-Bun-built native binary.
# setup-bun caches the canary binary by commit hash — same canary
# build is restored from cache, new build re-downloads.
uses: oven-sh/setup-bun@v2
with:
bun-version: canary
- name: Cache ~/.npm (claude-code-<plat> tarball)
# install.sh runs `npm pack @anthropic-ai/claude-code-linux-x64@latest`,
# which lands the ~80 MB tarball in ~/.npm/_cacache. Reusing the cache
# lets npm short-circuit the download when @latest hasn't bumped since
# the last run; it still re-validates registry metadata, so we never
# serve a stale binary on a real upgrade day.
uses: actions/cache@v4
with:
path: ~/.npm
key: npm-claude-${{ runner.os }}-${{ github.run_id }}
restore-keys: |
npm-claude-${{ runner.os }}-
- name: Install ripgrep
shell: bash
run: |
sudo apt-get update -qq && sudo apt-get install -y ripgrep
rg --version | head -1
- name: Add ~/.local/bin to PATH (clawgod launcher install target)
shell: bash
run: echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Print runtime versions
shell: bash
run: |
bun --version
node --version
uname -a
- name: Run install.sh (npm-fallback path)
# No official Claude binary pre-installed → install.sh exercises
# its npm-registry fallback to fetch @anthropic-ai/claude-code-<plat>.
# Capture output so the patch-result line can be asserted on; we
# don't trust `patch.mjs --verify` because some patches have no
# post-state sentinel (they use a regex-stale heuristic that
# false-positives once the source survives a partial replacement).
shell: bash
run: |
set -o pipefail
bash install.sh 2>&1 | tee /tmp/install.log
- name: Verify install artifacts
shell: bash
run: |
ls -la "$HOME/.clawgod/" || true
for f in cli.cjs cli.original.cjs patch.mjs extract-natives.mjs post-process.mjs .source-version; do
test -e "$HOME/.clawgod/$f" || { echo "::error::missing ~/.clawgod/$f"; exit 1; }
done
echo "Source: $(cat "$HOME/.clawgod/.source-version")"
- name: Assert all patches applied (parse install log)
# patch.mjs prints "Result: A applied, S skipped, F failed".
# F must be 0 — otherwise upstream has shifted enough that one of
# our regex patches no longer matches and a feature is silently broken.
shell: bash
run: |
line=$(grep -E 'Result: [0-9]+ applied, [0-9]+ skipped, [0-9]+ failed' /tmp/install.log | tail -1 || true)
echo "Patch summary: ${line:-<not found>}"
[[ -n "$line" ]] || { echo "::error::patch.mjs result line missing from install.sh output"; exit 1; }
failed=$(echo "$line" | sed -E 's/.*skipped, ([0-9]+) failed.*/\1/')
[[ "$failed" -eq 0 ]] || { echo "::error::patch.mjs reported $failed failed patches"; exit 1; }
- name: Smoke test — claude --version
# Exercises the wrapper path:
# launcher → bun cli.cjs → require('./cli.original.cjs')
# The Bun CJS-wrapper panic surfaces here, not at install time.
shell: bash
run: |
set +e
out=$(claude --version 2>&1)
rc=$?
set -e
echo "$out"
if echo "$out" | grep -q "Expected CommonJS module to have a function wrapper"; then
echo "::error::Bun CJS-wrapper panic — local Bun lags the embedded Bun"
exit 1
fi
[[ $rc -eq 0 ]] || { echo "::error::claude --version exited $rc"; exit 1; }
- name: Publish supported-Claude-version badge
# Write the version we just verified end-to-end to a JSON file on the
# `badges` branch (force-pushed; that branch holds nothing else and is
# never read as source). README links to it via shields.io endpoint.
# PRs from forks can't push, so skip them.
if: success() && github.event_name != 'pull_request'
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
version=$(cat "$HOME/.clawgod/.source-version")
[[ -n "$version" ]] || { echo "::error::source-version missing"; exit 1; }
tmp=$(mktemp -d)
cd "$tmp"
cat > claude-version.json <<EOF
{
"schemaVersion": 1,
"label": "Claude tested",
"message": "$version",
"color": "brightgreen"
}
EOF
git init -q -b badges
git config user.email "github-actions[bot]@users.noreply.github.com"
git config user.name "github-actions[bot]"
git add claude-version.json
git commit -q -m "compat-daily: claude $version verified"
git push -fq "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" HEAD:badges
echo "Published badge: claude $version"
- name: Open / update issue on scheduled failure
if: failure() && github.event_name == 'schedule'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
const stamp = path.join(process.env.HOME, '.clawgod', '.source-version');
const claudeVersion = fs.existsSync(stamp)
? fs.readFileSync(stamp, 'utf8').trim()
: 'unknown';
const title = `compat-daily: broke (claude ${claudeVersion})`;
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const { data: open } = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'compat-broken',
per_page: 100,
});
const dup = open.find(i => i.title === title);
if (dup) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: dup.number,
body: `Re-failed ${new Date().toISOString().slice(0,10)} — ${runUrl}`,
});
} else {
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title,
labels: ['compat-broken', 'bug'],
body: [
`Daily compatibility run failed on \`ubuntu-latest\`.`,
``,
`- Claude binary: \`${claudeVersion}\``,
`- Run: ${runUrl}`,
``,
`Auto-opened by \`.github/workflows/compat-daily.yml\`.`,
].join('\n'),
});
}
-84
View File
@@ -1,84 +0,0 @@
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
# Project policy: never run JS-based actions on Node 20 — they're already
# deprecated and forced off after 2026-06-02. Pin every action that runs
# JS (actions/checkout etc.) to Node 24 across all our workflows.
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Resolve versions
id: versions
run: |
tag="${GITHUB_REF_NAME}"
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
prev=$(git tag --sort=-version:refname | grep -v "^${tag}$" | head -n1)
echo "prev=${prev}" >> "$GITHUB_OUTPUT"
if [ -n "$prev" ]; then
echo "range=${prev}..${tag}" >> "$GITHUB_OUTPUT"
else
echo "range=${tag}" >> "$GITHUB_OUTPUT"
fi
- name: Build release notes
id: notes
run: |
tag="${{ steps.versions.outputs.tag }}"
prev="${{ steps.versions.outputs.prev }}"
range="${{ steps.versions.outputs.range }}"
{
echo "## Changes"
echo
if [ -n "$prev" ]; then
git log --pretty='- %s (%h)' "$range"
else
git log --pretty='- %s (%h)' "$tag"
fi
echo
echo "## Install"
echo
echo '**macOS / Linux:**'
echo '```bash'
echo "curl -fsSL https://github.com/${{ github.repository }}/releases/latest/download/install.sh | bash"
echo '```'
echo
echo '**Windows (PowerShell):**'
echo '```powershell'
echo "irm https://github.com/${{ github.repository }}/releases/latest/download/install.ps1 | iex"
echo '```'
if [ -n "$prev" ]; then
echo
echo "**Full changelog:** https://github.com/${{ github.repository }}/compare/${prev}...${tag}"
fi
} > release-notes.md
- name: Create GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${{ steps.versions.outputs.tag }}"
if gh release view "$tag" >/dev/null 2>&1; then
# Release already exists (e.g. created manually with curated notes).
# Refresh assets only — don't touch notes/title.
gh release upload "$tag" install.sh install.ps1 --clobber
else
gh release create "$tag" install.sh install.ps1 \
--title "ClawGod $tag" \
--notes-file release-notes.md
fi