Files
codewhale/.github/workflows/sync-cnb.yml
T
Hunter Bown 90eaf04a84 ci(cnb): rewrite sync workflow with concurrency + scoped push + retry
Closes the persistent "fatal: could not read Username for 'https://cnb.cool'"
failure that bit about half of recent sync-cnb runs.

Root causes from the failure log of run 25705666413 (2026-05-12) and
25697452832 (2026-05-11):

  1. The previous tencentcom/git-sync Docker action discovered every
     local ref via fetch-depth: 0 and tried to push them all —
     including dependabot/* branches that GitHub had locally. Those
     follow-on pushes ran without the configured credential helper
     in scope and failed with the basic-auth prompt error above.

  2. No concurrency guard meant the back-to-back `main` push and
     `v*` tag push that auto-tag.yml fires within ~15s of each other
     raced. Two workflow runs would attempt to push to the same
     CNB remote simultaneously; one would lose.

Rewrite:

  * Hand-rolled `git push` with the CNB token URL-encoded inline so
    the credential is always in scope. No Docker action dependency.
  * `concurrency: group: cnb-sync, cancel-in-progress: false` so
    queued runs serialize cleanly rather than racing or dropping.
  * Only push the ref that triggered the run — `main` on branch
    push (with --force-with-lease for safety), the specific tag on
    tag push. Feature branches and dependabot refs no longer
    mirror.
  * 3-attempt retry with linear backoff (5s, 10s) for transient
    failures (CNB rate-limit, DNS blips, etc.).
  * `workflow_dispatch: {}` trigger so the maintainer can re-run
    against a specific ref manually if the automated run fails.

Adds docs/CNB_MIRROR.md with: the verification steps after a
release, the manual fallback procedure (one-time `git remote add cnb`,
then `git push cnb vX.Y.Z`), the token-rotation procedure, and a
note on why binary release assets aren't on CNB today.

Cross-links from docs/RELEASE_RUNBOOK.md so the verify-after-release
step doesn't get forgotten.
2026-05-11 22:47:54 -05:00

105 lines
4.1 KiB
YAML

name: Sync to CNB
# Mirror commits and release tags to cnb.cool/deepseek-tui.com/DeepSeek-TUI
# so users behind GitHub-blocking networks can fetch the source and tagged
# releases from the Tencent-hosted mirror.
#
# Triggers:
# * push to main → mirrors that commit to CNB main
# * tag matching v* → mirrors that tag to CNB
# * workflow_dispatch → manual fallback if either of the above fails
#
# Why the rewrite (v0.8.31):
# The previous implementation used the opaque tencentcom/git-sync Docker
# action, which discovered every local ref via fetch-depth: 0 and tried
# to push them all — including dependabot/* branches that GitHub had
# locally. Those follow-on pushes ran without the configured credential
# helper in scope and failed with
# `fatal: could not read Username for 'https://cnb.cool'`
# No concurrency block meant the main-push and tag-push workflow runs
# that auto-tag.yml fires within seconds of each other raced. About
# half of recent runs failed for those two reasons combined.
on:
push:
branches: [main]
tags: ['v*']
workflow_dispatch: {}
# Serialize runs so the back-to-back main-push + tag-push from auto-tag.yml
# don't race each other rebasing onto CNB. cancel-in-progress: false so
# every commit actually arrives — we'd rather queue than drop.
concurrency:
group: cnb-sync
cancel-in-progress: false
permissions:
contents: read
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Push triggering ref to CNB
env:
CNB_TOKEN: ${{ secrets.CNB_GIT_TOKEN }}
shell: bash
run: |
set -euo pipefail
if [ -z "${CNB_TOKEN:-}" ]; then
echo "::error::CNB_GIT_TOKEN secret is not set; cannot push to CNB." >&2
exit 1
fi
# URL-encode any '%' in the token so basic-auth doesn't break on
# special characters. CNB tokens are typically alphanumeric so
# this is belt-and-suspenders.
ENCODED_TOKEN="$(printf '%s' "${CNB_TOKEN}" | python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.stdin.read(), safe=""))')"
REMOTE_URL="https://cnb:${ENCODED_TOKEN}@cnb.cool/deepseek-tui.com/DeepSeek-TUI.git"
# Use a masked alias so the token never appears in log lines.
git remote add cnb "${REMOTE_URL}"
# Push with retry on transient failures (CNB rate-limits, DNS
# blips, etc.). Args after `kind` are forwarded to `git push`
# so callers can pass `--force-with-lease`, multiple refspecs,
# etc. without quoting them into one string.
push_with_retry() {
local kind="$1"
shift
local attempt
for attempt in 1 2 3; do
echo "Attempt ${attempt}: pushing ${kind} to CNB"
if git push cnb "$@" 2>&1; then
echo "Successfully pushed ${kind} to CNB"
return 0
fi
if [ "${attempt}" -lt 3 ]; then
sleep $((attempt * 5))
fi
done
echo "::error::Failed to push ${kind} to CNB after 3 attempts" >&2
return 1
}
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
TAG="${GITHUB_REF#refs/tags/}"
push_with_retry "tag ${TAG}" "refs/tags/${TAG}:refs/tags/${TAG}"
elif [[ "${GITHUB_REF}" == refs/heads/main ]]; then
# --force-with-lease so an unexpected diverged state on CNB
# surfaces as a failure (rather than silently overwriting).
# The mirror is one-way; if CNB diverges, that's a bug worth
# investigating manually before pushing again.
push_with_retry "main" HEAD:refs/heads/main --force-with-lease
else
# workflow_dispatch from a non-main branch — push that branch
# too, but never force. Useful for testing the mirror against
# a feature branch before merging.
BRANCH="${GITHUB_REF#refs/heads/}"
push_with_retry "branch ${BRANCH}" "HEAD:refs/heads/${BRANCH}"
fi