Mirror the retry loop already used in desktop-build.yml for every remaining one-shot 'bun install --frozen-lockfile' step so transient registry or network failures (e.g. a failed mermaid tarball download) no longer fail the whole run. deploy-tools.yml gains shell: bash because its matrix includes windows-latest; sync-edgeever-upstream.yml uses break on success so the following verification commands still run.
422 lines
20 KiB
YAML
422 lines
20 KiB
YAML
name: Update deployed EdgeEver
|
|
|
|
# Keeps a deployment Fork aligned with upstream EdgeEver.
|
|
# Deployment Forks mirror product code by default while preserving a small
|
|
# updater layer so GITHUB_TOKEN never needs permission to rewrite Actions.
|
|
# The rare customized Fork must explicitly opt in to merge preservation.
|
|
|
|
on:
|
|
schedule:
|
|
- cron: "17 2 * * *"
|
|
workflow_dispatch:
|
|
inputs:
|
|
channel:
|
|
description: Update channel for this run (stable = latest formal Release, edge = upstream main)
|
|
required: true
|
|
default: stable
|
|
type: choice
|
|
options:
|
|
- stable
|
|
- edge
|
|
permissions:
|
|
contents: write
|
|
issues: write
|
|
|
|
concurrency:
|
|
group: edgeever-upstream-update
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
update:
|
|
if: github.repository != 'tianma-if/edgeever'
|
|
name: Sync Fork and trigger deployment
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
UPSTREAM_REPOSITORY: tianma-if/edgeever
|
|
UPDATE_CHANNEL: ${{ inputs.channel || vars.EDGE_EVER_UPDATE_CHANNEL || 'stable' }}
|
|
# A manual run is an explicit request to publish the selected version.
|
|
# Scheduled checks remain no-op when the Fork is already aligned.
|
|
FORCE_REDEPLOY: ${{ github.event_name == 'workflow_dispatch' }}
|
|
PRESERVE_FORK_CHANGES: ${{ vars.EDGE_EVER_PRESERVE_FORK_CHANGES || 'false' }}
|
|
|
|
steps:
|
|
- name: Checkout deployed repository
|
|
uses: actions/checkout@v5
|
|
with:
|
|
ref: main
|
|
fetch-depth: 0
|
|
|
|
- name: Require a GitHub Fork
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
is_fork="$(gh api "repos/${GITHUB_REPOSITORY}" --jq '.fork')"
|
|
if [ "${is_fork}" != "true" ]; then
|
|
echo "This deployment repository must be a GitHub Fork of ${UPSTREAM_REPOSITORY}." >&2
|
|
echo "Create the repository with the EdgeEver Fork flow before enabling upstream updates." >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: Resolve upstream version
|
|
id: upstream
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
git remote add upstream "https://github.com/${UPSTREAM_REPOSITORY}.git"
|
|
git fetch upstream main --tags
|
|
|
|
case "${UPDATE_CHANNEL}" in
|
|
stable)
|
|
target_ref="$(gh api "repos/${UPSTREAM_REPOSITORY}/releases/latest" --jq '.tag_name')"
|
|
;;
|
|
edge)
|
|
target_ref="upstream/main"
|
|
;;
|
|
*)
|
|
echo "Unsupported update channel: ${UPDATE_CHANNEL}" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
target_commit="$(git rev-parse "${target_ref}^{commit}")"
|
|
head_commit="$(git rev-parse HEAD)"
|
|
target_version="$(git show "${target_commit}:package.json" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("version",""))' 2>/dev/null || true)"
|
|
if [ -z "${target_version}" ]; then
|
|
target_version="unknown"
|
|
fi
|
|
head_version="$(python3 -c 'import json; print(json.load(open("package.json")).get("version",""))' 2>/dev/null || true)"
|
|
if [ -z "${head_version}" ]; then
|
|
head_version="unknown"
|
|
fi
|
|
|
|
# GitHub blocks GITHUB_TOKEN pushes that add, modify, or remove Actions
|
|
# workflows. Deployment forks therefore compare and update only the
|
|
# managed product tree; the small downstream updater layer stays local.
|
|
content_diff="$(git diff --name-only "${target_commit}" HEAD -- . \
|
|
':(exclude).github/workflows/**' \
|
|
':(exclude)scripts/prepare-upstream-sync.mjs' \
|
|
':(exclude)scripts/upstream-sync-plan.mjs' || true)"
|
|
content_matches_target=false
|
|
if [ -z "${content_diff}" ]; then
|
|
content_matches_target=true
|
|
fi
|
|
|
|
fork_mode=mirror
|
|
if [ "${PRESERVE_FORK_CHANGES}" = "true" ]; then
|
|
fork_mode=customized
|
|
fi
|
|
|
|
head_equals_target=false
|
|
if [ "${head_commit}" = "${target_commit}" ]; then
|
|
head_equals_target=true
|
|
fi
|
|
head_is_ancestor_of_target=false
|
|
if git merge-base --is-ancestor HEAD "${target_commit}" 2>/dev/null; then
|
|
head_is_ancestor_of_target=true
|
|
fi
|
|
target_is_ancestor_of_head=false
|
|
if git merge-base --is-ancestor "${target_commit}" HEAD 2>/dev/null; then
|
|
target_is_ancestor_of_head=true
|
|
fi
|
|
|
|
planner_path="${RUNNER_TEMP}/upstream-sync-plan.mjs"
|
|
if git cat-file -e "${target_commit}:scripts/upstream-sync-plan.mjs" 2>/dev/null; then
|
|
git show "${target_commit}:scripts/upstream-sync-plan.mjs" > "${planner_path}"
|
|
else
|
|
cp scripts/upstream-sync-plan.mjs "${planner_path}"
|
|
fi
|
|
|
|
IFS=$'\t' read -r update_required align_mode reason republish_only < <(
|
|
EDGE_SYNC_CONTENT_MATCHES_TARGET="${content_matches_target}" \
|
|
EDGE_SYNC_FORCE_REDEPLOY="${FORCE_REDEPLOY}" \
|
|
EDGE_SYNC_HEAD_EQUALS_TARGET="${head_equals_target}" \
|
|
EDGE_SYNC_HEAD_IS_ANCESTOR_OF_TARGET="${head_is_ancestor_of_target}" \
|
|
EDGE_SYNC_PRESERVE_FORK_CHANGES="${PRESERVE_FORK_CHANGES}" \
|
|
EDGE_SYNC_TARGET_IS_ANCESTOR_OF_HEAD="${target_is_ancestor_of_head}" \
|
|
node "${planner_path}"
|
|
)
|
|
# Releases created before the snapshot updater called this mode reset.
|
|
# Normalize that legacy plan without executing a history rewrite.
|
|
if [ "${align_mode}" = "reset" ]; then
|
|
align_mode=snapshot
|
|
fi
|
|
|
|
{
|
|
echo "target_ref=${target_ref}"
|
|
echo "target_commit=${target_commit}"
|
|
echo "target_version=${target_version}"
|
|
echo "head_commit=${head_commit}"
|
|
echo "head_version=${head_version}"
|
|
echo "update_required=${update_required}"
|
|
echo "align_mode=${align_mode}"
|
|
echo "reason=${reason}"
|
|
echo "republish_only=${republish_only}"
|
|
} >> "${GITHUB_OUTPUT}"
|
|
|
|
{
|
|
echo "### EdgeEver update / EdgeEver 更新"
|
|
echo ""
|
|
echo "| Field / 字段 | Value / 值 |"
|
|
echo "| --- | --- |"
|
|
echo "| Channel / 更新通道 | \`${UPDATE_CHANNEL}\` |"
|
|
echo "| Fork mode / Fork 模式 | \`${fork_mode}\` |"
|
|
echo "| Upstream target / 上游目标 | \`${target_ref}\` (\`${target_commit:0:12}\`, v${target_version}) |"
|
|
echo "| Fork main before / 更新前 | \`${head_commit:0:12}\` (v${head_version}) |"
|
|
echo "| Decision / 判定 | \`${reason}\` / align=\`${align_mode}\` / update=\`${update_required}\` / republish=\`${republish_only}\` |"
|
|
echo ""
|
|
case "${reason}" in
|
|
already_on_target)
|
|
echo "**Already on upstream target.** No git update is needed. This is success for a read-only deployment fork — not a skipped failure."
|
|
;;
|
|
content_matches_target)
|
|
echo "**Application content already matches upstream target** (history tip may differ). No git update is needed for deployment."
|
|
;;
|
|
behind_target)
|
|
echo "**Fork is behind upstream.** Applying the target product snapshot while preserving downstream workflows."
|
|
;;
|
|
deploy_mirror_reset)
|
|
echo "**Deployment mirror differs from upstream.** Replacing managed product files with the selected target while preserving downstream workflows."
|
|
;;
|
|
deploy_mirror_ahead)
|
|
echo "**Deploy-only fork is ahead of the selected upstream target.** Applying that target's product snapshot while preserving downstream workflows."
|
|
;;
|
|
customized_contains_target)
|
|
echo "**Opt-in customized fork already contains the upstream target.** Preserving its application changes without another merge."
|
|
;;
|
|
customized_merge)
|
|
echo "**Opt-in customized fork.** Merging upstream target and leaving production unchanged if verification fails."
|
|
;;
|
|
*)
|
|
echo "Decision reason: \`${reason}\`."
|
|
;;
|
|
esac
|
|
if [ "${republish_only}" = "true" ]; then
|
|
echo ""
|
|
echo "**Manual redeploy / 手动重新部署:** the Fork is already aligned, so this run will push an empty commit and request a fresh Cloudflare build."
|
|
fi
|
|
echo ""
|
|
echo "Prefer this workflow over GitHub **Sync fork**. Mirror mode owns the managed product tree while preserving the Fork's local updater layer and \`.github/workflows/**\`."
|
|
} >> "${GITHUB_STEP_SUMMARY}"
|
|
|
|
if [ "${republish_only}" = "true" ]; then
|
|
echo "::notice title=Manual redeploy / 手动重新部署::Fork code is already v${target_version}. This run will push an empty commit and request a fresh Cloudflare build."
|
|
elif [ "${update_required}" = "true" ]; then
|
|
echo "::notice title=EdgeEver update / EdgeEver 更新::Fork main v${head_version} will be synchronized to v${target_version}, then a Cloudflare build will be requested."
|
|
else
|
|
echo "::notice title=Scheduled check / 定时检查::Fork code is already v${target_version}. No Git push or Cloudflare deployment was requested by this run."
|
|
fi
|
|
|
|
echo "channel=${UPDATE_CHANNEL} target=${target_ref}@${target_commit:0:12} (v${target_version}) head=${head_commit:0:12} (v${head_version}) reason=${reason} align=${align_mode} update=${update_required} republish=${republish_only}"
|
|
|
|
- name: Align to upstream
|
|
if: steps.upstream.outputs.update_required == 'true'
|
|
env:
|
|
TARGET_COMMIT: ${{ steps.upstream.outputs.target_commit }}
|
|
TARGET_REF: ${{ steps.upstream.outputs.target_ref }}
|
|
ALIGN_MODE: ${{ steps.upstream.outputs.align_mode }}
|
|
REASON: ${{ steps.upstream.outputs.reason }}
|
|
run: |
|
|
set -euo pipefail
|
|
git config user.name "edgeever-updater[bot]"
|
|
git config user.email "edgeever-updater[bot]@users.noreply.github.com"
|
|
|
|
helper_path="${RUNNER_TEMP}/prepare-upstream-sync.mjs"
|
|
if git cat-file -e "${TARGET_COMMIT}:scripts/prepare-upstream-sync.mjs" 2>/dev/null; then
|
|
git show "${TARGET_COMMIT}:scripts/prepare-upstream-sync.mjs" > "${helper_path}"
|
|
else
|
|
cp scripts/prepare-upstream-sync.mjs "${helper_path}"
|
|
fi
|
|
|
|
EDGE_SYNC_ALIGN_MODE="${ALIGN_MODE}" \
|
|
EDGE_SYNC_BASE_COMMIT="HEAD" \
|
|
EDGE_SYNC_TARGET_COMMIT="${TARGET_COMMIT}" \
|
|
node "${helper_path}" prepare
|
|
echo "Prepared ${ALIGN_MODE} update from ${TARGET_REF} (${TARGET_COMMIT:0:12}) because ${REASON}."
|
|
|
|
- name: Set up Bun
|
|
if: steps.upstream.outputs.align_mode == 'merge'
|
|
uses: oven-sh/setup-bun@v2
|
|
|
|
- name: Verify customized merge
|
|
if: steps.upstream.outputs.align_mode == 'merge'
|
|
run: |
|
|
set -euo pipefail
|
|
for attempt in 1 2 3; do
|
|
if bun install --frozen-lockfile; then
|
|
break
|
|
fi
|
|
if [[ "$attempt" = "3" ]]; then
|
|
exit 1
|
|
fi
|
|
sleep "$((15 * attempt))"
|
|
done
|
|
bun run db:migrate:local
|
|
bun run test
|
|
bun run typecheck
|
|
bun run build
|
|
|
|
- name: Publish update
|
|
id: publish
|
|
if: steps.upstream.outputs.update_required == 'true' || steps.upstream.outputs.republish_only == 'true'
|
|
env:
|
|
TARGET_REF: ${{ steps.upstream.outputs.target_ref }}
|
|
TARGET_COMMIT: ${{ steps.upstream.outputs.target_commit }}
|
|
UPDATE_REQUIRED: ${{ steps.upstream.outputs.update_required }}
|
|
REPUBLISH_ONLY: ${{ steps.upstream.outputs.republish_only }}
|
|
ALIGN_MODE: ${{ steps.upstream.outputs.align_mode }}
|
|
REASON: ${{ steps.upstream.outputs.reason }}
|
|
run: |
|
|
set -euo pipefail
|
|
git config user.name "edgeever-updater[bot]"
|
|
git config user.email "edgeever-updater[bot]@users.noreply.github.com"
|
|
|
|
if [ "${REPUBLISH_ONLY}" = "true" ] && [ "${UPDATE_REQUIRED}" != "true" ]; then
|
|
git commit --allow-empty -m "chore: retrigger EdgeEver deployment for ${TARGET_REF}"
|
|
git push origin HEAD:main
|
|
echo "mode=empty_commit" >> "${GITHUB_OUTPUT}"
|
|
echo "Pushed empty commit to retrigger Cloudflare Workers Builds."
|
|
exit 0
|
|
fi
|
|
|
|
git commit \
|
|
-m "chore: update EdgeEver from ${TARGET_REF}" \
|
|
-m "EdgeEver-Upstream-Commit: ${TARGET_COMMIT}"
|
|
|
|
if ! git diff --quiet HEAD^ HEAD -- \
|
|
.github/workflows \
|
|
scripts/prepare-upstream-sync.mjs \
|
|
scripts/upstream-sync-plan.mjs; then
|
|
echo "Refusing to publish an update that changes the downstream updater layer." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ "${ALIGN_MODE}" = "snapshot" ] && \
|
|
! git diff --quiet "${TARGET_COMMIT}" HEAD -- . \
|
|
':(exclude).github/workflows/**' \
|
|
':(exclude)scripts/prepare-upstream-sync.mjs' \
|
|
':(exclude)scripts/upstream-sync-plan.mjs'; then
|
|
echo "Prepared product tree does not match ${TARGET_REF}." >&2
|
|
exit 1
|
|
fi
|
|
|
|
git push origin HEAD:main
|
|
echo "mode=${ALIGN_MODE}_commit" >> "${GITHUB_OUTPUT}"
|
|
echo "Published ${ALIGN_MODE} update for ${TARGET_REF} without changing downstream workflows."
|
|
|
|
- name: Request Cloudflare deployment
|
|
id: deploy
|
|
if: steps.publish.outcome == 'success'
|
|
env:
|
|
DEPLOY_HOOK_URL: ${{ secrets.EDGE_EVER_CLOUDFLARE_DEPLOY_HOOK_URL }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [ -z "${DEPLOY_HOOK_URL:-}" ]; then
|
|
echo "No EDGE_EVER_CLOUDFLARE_DEPLOY_HOOK_URL secret — relying on Cloudflare Git integration for main pushes."
|
|
echo "Optional: add a Workers/Pages Deploy Hook secret if pushes do not start builds." >> "${GITHUB_STEP_SUMMARY}"
|
|
echo "method=git_push" >> "${GITHUB_OUTPUT}"
|
|
exit 0
|
|
fi
|
|
curl -fsS -X POST "${DEPLOY_HOOK_URL}" >/dev/null
|
|
echo "Triggered Cloudflare Deploy Hook."
|
|
echo "method=git_push_and_deploy_hook" >> "${GITHUB_OUTPUT}"
|
|
echo "" >> "${GITHUB_STEP_SUMMARY}"
|
|
echo "Triggered optional Cloudflare Deploy Hook after publish." >> "${GITHUB_STEP_SUMMARY}"
|
|
|
|
- name: Report result / 输出结果
|
|
if: always() && steps.upstream.outcome == 'success'
|
|
env:
|
|
TARGET_REF: ${{ steps.upstream.outputs.target_ref }}
|
|
TARGET_COMMIT: ${{ steps.upstream.outputs.target_commit }}
|
|
TARGET_VERSION: ${{ steps.upstream.outputs.target_version }}
|
|
UPDATE_REQUIRED: ${{ steps.upstream.outputs.update_required }}
|
|
REPUBLISH_ONLY: ${{ steps.upstream.outputs.republish_only }}
|
|
REASON: ${{ steps.upstream.outputs.reason }}
|
|
ALIGN_MODE: ${{ steps.upstream.outputs.align_mode }}
|
|
PUBLISH_OUTCOME: ${{ steps.publish.outcome }}
|
|
PUBLISH_MODE: ${{ steps.publish.outputs.mode }}
|
|
DEPLOY_TRIGGER_OUTCOME: ${{ steps.deploy.outcome }}
|
|
DEPLOY_TRIGGER_METHOD: ${{ steps.deploy.outputs.method }}
|
|
run: |
|
|
set -euo pipefail
|
|
final_commit="$(git rev-parse HEAD)"
|
|
git_result="Not requested / 未请求"
|
|
deploy_result="Not requested / 未请求"
|
|
live_result="Not changed by this run / 本次运行未改变线上部署"
|
|
live_status="unchanged"
|
|
notice_level="notice"
|
|
notice_title="Scheduled check / 定时检查"
|
|
notice_body="Fork code is already v${TARGET_VERSION}. No Git push or Cloudflare deployment was requested."
|
|
|
|
if [ "${UPDATE_REQUIRED}" = "true" ] || [ "${REPUBLISH_ONLY}" = "true" ]; then
|
|
live_result="Not verified by this workflow / 本工作流未验证"
|
|
live_status="not_verified"
|
|
if [ "${PUBLISH_OUTCOME}" != "success" ]; then
|
|
git_result="Failed (\`${PUBLISH_OUTCOME}\`) / 失败"
|
|
deploy_result="Not requested because Git publish failed / 因 Git 发布失败而未请求"
|
|
notice_level="error"
|
|
notice_title="Git publish failed / Git 发布失败"
|
|
notice_body="The Fork could not publish v${TARGET_VERSION}; no Cloudflare deployment was requested."
|
|
else
|
|
case "${PUBLISH_MODE}" in
|
|
empty_commit)
|
|
git_result="Redeploy commit pushed / 已推送重部署 commit"
|
|
;;
|
|
*)
|
|
git_result="Version update pushed (\`${ALIGN_MODE}\`) / 已推送版本更新"
|
|
;;
|
|
esac
|
|
|
|
if [ "${DEPLOY_TRIGGER_OUTCOME}" = "success" ]; then
|
|
case "${DEPLOY_TRIGGER_METHOD}" in
|
|
git_push_and_deploy_hook)
|
|
deploy_result="Requested by Git push and Deploy Hook / 已通过 Git push 和 Deploy Hook 请求"
|
|
;;
|
|
*)
|
|
deploy_result="Requested by Git push / 已通过 Git push 请求"
|
|
;;
|
|
esac
|
|
notice_title="Deployment requested / 已请求部署"
|
|
notice_body="Fork main is ${final_commit:0:12} (v${TARGET_VERSION}). Confirm Cloudflare Deployments builds this commit; the live version is not verified here."
|
|
else
|
|
deploy_result="Trigger failed (\`${DEPLOY_TRIGGER_OUTCOME}\`) / 触发失败"
|
|
notice_level="error"
|
|
notice_title="Deployment trigger failed / 部署触发失败"
|
|
notice_body="Fork main is ${final_commit:0:12} (v${TARGET_VERSION}), but the Cloudflare deployment request failed."
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
{
|
|
echo ""
|
|
echo "### Result / 结果"
|
|
echo ""
|
|
echo "| Field / 字段 | Value / 值 |"
|
|
echo "| --- | --- |"
|
|
echo "| Target version / 目标版本 | \`${TARGET_REF}\` / v${TARGET_VERSION} |"
|
|
echo "| Final main / 最终 main | \`${final_commit:0:12}\` |"
|
|
echo "| Git publish / Git 发布 | ${git_result} |"
|
|
echo "| Deployment trigger / 部署触发 | ${deploy_result} |"
|
|
echo "| Live deployment / 线上部署 | ${live_result} |"
|
|
echo ""
|
|
echo "When a deployment was requested, confirm Cloudflare **Deployments** builds commit \`${final_commit:0:12}\`. / 若已请求部署,请在 Cloudflare **Deployments** 中确认构建 commit 为 \`${final_commit:0:12}\`。"
|
|
} >> "${GITHUB_STEP_SUMMARY}"
|
|
echo "::${notice_level} title=${notice_title}::${notice_body}"
|
|
echo "result target=v${TARGET_VERSION} final=${final_commit:0:12} git_publish=${PUBLISH_OUTCOME} publish_mode=${PUBLISH_MODE:-none} deploy_trigger=${DEPLOY_TRIGGER_OUTCOME} deploy_method=${DEPLOY_TRIGGER_METHOD:-none} live=${live_status}"
|
|
|
|
- name: Report an update failure
|
|
if: failure()
|
|
continue-on-error: true
|
|
env:
|
|
TARGET_REF: ${{ steps.upstream.outputs.target_ref || 'unknown' }}
|
|
run: |
|
|
set -euo pipefail
|
|
title="EdgeEver automatic update requires attention"
|
|
existing="$(gh issue list --state open --search "${title} in:title" --json number --jq '.[0].number // empty')"
|
|
if [ -z "${existing}" ]; then
|
|
gh issue create \
|
|
--title "${title}" \
|
|
--body "The daily update from \`${UPSTREAM_REPOSITORY}\` (${TARGET_REF}) could not be aligned or verified. The current production deployment was left unchanged. Open the failed **Update deployed EdgeEver** workflow run for details (see the job summary for channel/target/reason), then retry it manually after resolving the problem."
|
|
fi
|