mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
cicd.yml builds a release on `tags: ['v*']`, and the sync only ever moved refs/heads/main. Tags are created on GitHub, so they never arrived here, the trigger never fired, and no semver image was ever built: the forge sat at v1.26.24 while GitHub had v1.26.26. A version number that names no artifact is worse than no tag at all, because it reads as a release. Tags are immutable, so fast-forward-only means a tag we do not have is pushed, and a tag we do have that points somewhere else is a rewritten release — which is reported and refused, never silently adopted and never silently skipped. Both sides are read from their own ref namespace. A draft that compared against refs/tags/ was wrong in a way worth recording: `git fetch` auto-follows tags pointing at fetched objects even when given an explicit refspec, so the GitHub tags landed in refs/tags/ as a side effect, every new tag compared equal to the copy just followed, and nothing was ever pushed — this fix silently undoing itself. --no-tags plus an explicit namespace per side removes both that and any dependence on what checkout leaves behind. Verified against a pair of local repositories holding a shared tag, a rewritten tag and a new one: unchanged is left alone, the rewrite is refused and reported, the new tag is pushed. Co-authored-by: Hanzo Dev <[email protected]>
111 lines
5.4 KiB
YAML
111 lines
5.4 KiB
YAML
name: Sync from GitHub
|
|
# git.hanzo.ai is canonical for this fork (the forge reports mirror=false — it is
|
|
# not a pull mirror of GitHub, it owns main); development also lands on
|
|
# github.com/hanzoai/git. ONE deterministic direction: an in-cluster PULL. The
|
|
# runner reaches both ends (GitHub outbound, this instance via the URL
|
|
# actions/checkout already uses), so the sync has no ingress dependency.
|
|
#
|
|
# Fast-forward ONLY: a divergence fails loudly here instead of force-pushing
|
|
# either side.
|
|
on:
|
|
schedule:
|
|
- cron: '*/10 * * * *'
|
|
workflow_dispatch: {}
|
|
concurrency:
|
|
group: sync-from-github
|
|
cancel-in-progress: false
|
|
jobs:
|
|
ff-main:
|
|
runs-on: [hanzo-build-linux-amd64]
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
persist-credentials: true
|
|
- name: Fast-forward main from github.com/hanzoai/git
|
|
env:
|
|
GH_PAT: ${{ secrets.GH_PAT }}
|
|
run: |
|
|
set -euo pipefail
|
|
git fetch --quiet "https://x-access-token:${GH_PAT}@github.com/hanzoai/git.git" main
|
|
LOCAL="$(git rev-parse HEAD)"; REMOTE="$(git rev-parse FETCH_HEAD)"
|
|
if [ "$LOCAL" = "$REMOTE" ]; then echo "in sync at $LOCAL"; exit 0; fi
|
|
if git merge-base --is-ancestor "$LOCAL" "$REMOTE"; then
|
|
echo "fast-forwarding $LOCAL -> $REMOTE"
|
|
git push origin "$REMOTE:refs/heads/main"
|
|
# A push made with the workflow token does not trigger workflows, so
|
|
# synced commits would never build. Dispatch the build explicitly.
|
|
curl -fsS --max-time 20 -X POST \
|
|
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
|
-H "Content-Type: application/json" \
|
|
"${{ github.server_url }}/v1/repos/${{ github.repository }}/actions/workflows/cicd.yml/dispatches" \
|
|
-d '{"ref":"main"}' \
|
|
&& echo "CI/CD dispatched" || echo "CI/CD dispatch failed (non-fatal — next direct push builds)"
|
|
elif git merge-base --is-ancestor "$REMOTE" "$LOCAL"; then
|
|
echo "canonical is AHEAD of GitHub — nothing to pull (never force from here)."
|
|
else
|
|
echo "::error::main DIVERGED between GitHub ($REMOTE) and canonical ($LOCAL) — refusing to force. Reconcile manually."
|
|
exit 1
|
|
fi
|
|
|
|
# Tags were not synced, only main. cicd.yml triggers a release build on
|
|
# `tags: ['v*']`, and that tag is created on GitHub — so it never arrived
|
|
# here, the trigger never fired, and no semver image was ever built. The
|
|
# forge sat at v1.26.24 while GitHub had v1.26.26: tagged, and not built.
|
|
# A version number that names no artifact is worse than no tag, because it
|
|
# reads as a release.
|
|
#
|
|
# Tags are immutable, so "fast-forward only" means: a tag we do not have is
|
|
# pushed, and a tag we do have that points somewhere else is a rewritten
|
|
# release — which must be loud, never silently adopted or silently ignored.
|
|
- name: Sync tags from github.com/hanzoai/git
|
|
env:
|
|
GH_PAT: ${{ secrets.GH_PAT }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# Each side is fetched into its own namespace rather than over
|
|
# refs/tags/*. Two reasons, both of which broke a draft of this:
|
|
#
|
|
# * A plain `fetch --tags` refuses to move a tag that already exists
|
|
# locally and fails the step — turning the very case this step must
|
|
# REPORT into a fetch error.
|
|
#
|
|
# * `git fetch` auto-follows tags that point at fetched objects even
|
|
# when given an explicit refspec, so refs/tags/ picks up GitHub's
|
|
# tags as a side effect. Comparing against refs/tags/ then finds the
|
|
# tag we just followed, calls a genuinely new tag unchanged, and
|
|
# never pushes it — the exact failure this step exists to fix,
|
|
# silently reintroduced. --no-tags turns that off, and reading the
|
|
# canonical side from its own namespace means the comparison does
|
|
# not depend on what checkout happened to leave behind either.
|
|
git fetch --quiet --prune --no-tags "https://x-access-token:${GH_PAT}@github.com/hanzoai/git.git" \
|
|
'+refs/tags/*:refs/gh-tags/*'
|
|
git fetch --quiet --prune --no-tags origin \
|
|
'+refs/tags/*:refs/canon-tags/*'
|
|
|
|
new=0 conflict=0
|
|
for ref in $(git for-each-ref --format='%(refname)' refs/gh-tags/); do
|
|
tag="${ref#refs/gh-tags/}"
|
|
# Compared unpeeled: for an annotated tag this is the tag object on
|
|
# both sides, so a tag is equal to itself and only a genuine rewrite
|
|
# differs.
|
|
theirs="$(git rev-parse "$ref")"
|
|
mine="$(git rev-parse -q --verify "refs/canon-tags/${tag}" || true)"
|
|
|
|
if [ -z "$mine" ]; then
|
|
echo "new tag ${tag} -> ${theirs}"
|
|
git push origin "${ref}:refs/tags/${tag}"
|
|
new=$((new+1))
|
|
elif [ "$mine" != "$theirs" ]; then
|
|
echo "::error::tag ${tag} is ${mine} here and ${theirs} on GitHub — a released tag was rewritten. Refusing to move it."
|
|
conflict=$((conflict+1))
|
|
fi
|
|
done
|
|
|
|
echo "${new} tag(s) pushed"
|
|
if [ "$conflict" -ne 0 ]; then
|
|
echo "::error::${conflict} tag(s) disagree between GitHub and canonical — reconcile manually."
|
|
exit 1
|
|
fi
|