fix(deploy): keep fork updater bootstrap stable
This commit is contained in:
1 parent
06297ff11f
commit
d954ab7c29
7 files changed
+100
-43
No files matched your search
@@ -1,8 +1,8 @@
|
||||
name: Update deployed EdgeEver
|
||||
|
||||
# Keeps a deployment Fork aligned with upstream EdgeEver.
|
||||
# Deployment Forks mirror product code by default while preserving their local
|
||||
# workflow directory so GITHUB_TOKEN never needs permission to rewrite Actions.
|
||||
# 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:
|
||||
@@ -97,8 +97,11 @@ jobs:
|
||||
|
||||
# GitHub blocks GITHUB_TOKEN pushes that add, modify, or remove Actions
|
||||
# workflows. Deployment forks therefore compare and update only the
|
||||
# managed product tree; their complete workflow directory stays local.
|
||||
content_diff="$(git diff --name-only "${target_commit}" HEAD -- . ':(exclude).github/workflows/**' || true)"
|
||||
# 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
|
||||
@@ -122,6 +125,13 @@ jobs:
|
||||
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}" \
|
||||
@@ -129,8 +139,13 @@ jobs:
|
||||
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 scripts/upstream-sync-plan.mjs
|
||||
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}"
|
||||
@@ -186,7 +201,7 @@ jobs:
|
||||
echo "\`force_redeploy\` is set: will push an empty commit so Cloudflare Workers Builds runs again."
|
||||
fi
|
||||
echo ""
|
||||
echo "Prefer this workflow over GitHub **Sync fork**. Mirror mode owns the managed product tree while preserving the Fork's local \`.github/workflows/**\`."
|
||||
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}"
|
||||
|
||||
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}"
|
||||
@@ -259,13 +274,19 @@ jobs:
|
||||
-m "chore: update EdgeEver from ${TARGET_REF}" \
|
||||
-m "EdgeEver-Upstream-Commit: ${TARGET_COMMIT}"
|
||||
|
||||
if ! git diff --quiet HEAD^ HEAD -- .github/workflows; then
|
||||
echo "Refusing to publish an update that changes downstream GitHub Actions workflows." >&2
|
||||
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/**'; then
|
||||
! 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
|
||||
|
||||
@@ -14,7 +14,7 @@ This document defines the standard operating specifications and conventions for
|
||||
2. **Enable Workflow**
|
||||
- In the **Actions** tab of the Forked repository, enable the **Update deployed EdgeEver** automatic update workflow (required on public forks: scheduled workflows are disabled by default).
|
||||
- An ordinary Fork is a deployment mirror by default. No update variable is required: the workflow applies the selected upstream Release's product snapshot to `main` before triggering Cloudflare.
|
||||
- The updater preserves the Fork's complete `.github/workflows/**` directory. Official packaging, signing, testing, and Release workflows remain guarded to `tianma-if/edgeever` and are never rewritten by the downstream updater.
|
||||
- The updater preserves the Fork's complete `.github/workflows/**` directory and its two updater helper scripts as a stable local bootstrap layer. Official packaging, signing, testing, and Release workflows remain guarded to `tianma-if/edgeever` and are never rewritten by downstream product updates.
|
||||
- Only a Fork that intentionally maintains application-code changes should create the Actions repository variable `EDGE_EVER_PRESERVE_FORK_CHANGES=true`. This opts into merges and requires the owner to resolve future conflicts; it is not needed for normal deployments.
|
||||
|
||||
3. **Cloudflare Project Import**
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
2. **启用工作流**
|
||||
- 在 Fork 仓库的 **Actions** 标签页中,启用 **Update deployed EdgeEver** 自动更新工作流(公共 Fork 上定时任务默认关闭,必须手动启用)。
|
||||
- 普通 Fork 默认作为部署镜像,无需配置任何更新变量:工作流会把所选上游 Release 的产品代码快照应用到 `main`,再触发 Cloudflare。
|
||||
- 更新器会完整保留 Fork 自己的 `.github/workflows/**` 目录。官方打包、签名、测试与 Release 工作流继续通过 `tianma-if/edgeever` 仓库门禁限制执行,也不会被下游更新器改写。
|
||||
- 更新器会把 Fork 自己的完整 `.github/workflows/**` 目录及两个更新辅助脚本作为稳定的本地引导层保留。官方打包、签名、测试与 Release 工作流继续通过 `tianma-if/edgeever` 仓库门禁限制执行,也不会被下游产品更新改写。
|
||||
- 只有明确维护了应用代码修改的 Fork,才应创建 Actions 仓库变量 `EDGE_EVER_PRESERVE_FORK_CHANGES=true`。启用后改为 merge,并由仓库所有者处理未来冲突;普通部署无需设置。
|
||||
|
||||
3. **Cloudflare 项目导入**
|
||||
|
||||
@@ -19,7 +19,7 @@ Configure `EDGE_EVER_AUTH_PASSWORD` under the Worker's **Settings -> Variables a
|
||||
- Default channel `stable` tracks the latest formal Release tag.
|
||||
- Set the GitHub Repository Variable `EDGE_EVER_UPDATE_CHANNEL=edge` to follow upstream `main`.
|
||||
- Read-only forks (no app code changes) apply the target's product snapshot in a new linear commit; customized forks merge product changes and fail closed on conflicts.
|
||||
- The complete downstream `.github/workflows/**` directory is preserved. Official packaging, signing, testing, and Release workflows are not part of automatic product updates, so `GITHUB_TOKEN` never needs permission to rewrite Actions workflows.
|
||||
- The complete downstream `.github/workflows/**` directory and two updater helper scripts form a stable local bootstrap layer. Official packaging, signing, testing, and Release workflows are not part of automatic product updates, so `GITHUB_TOKEN` never needs permission to rewrite Actions workflows.
|
||||
- Every run writes a job **Summary** with channel, target version, decision reason, and whether a push happened. A green run that says *Already on upstream target* is success, not a silent failure.
|
||||
- Prefer this workflow over GitHub **Sync fork**. Sync fork follows upstream `main` history and can make the next stable run a deliberate no-op.
|
||||
- Optional: repository secret `EDGE_EVER_CLOUDFLARE_DEPLOY_HOOK_URL` triggers a Cloudflare Deploy Hook after a successful push (useful when the Git integration misses a push).
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
- 默认 `stable` 通道跟随最新正式 Release tag。
|
||||
- 设置 GitHub Repository Variable `EDGE_EVER_UPDATE_CHANNEL=edge` 后跟随上游 `main`。
|
||||
- 只读 Fork(未改应用代码)会用一个新的线性提交应用目标版本的产品代码快照;有本地业务改动的 Fork 合并产品代码,冲突则失败并保持线上不变。
|
||||
- 下游完整的 `.github/workflows/**` 目录会原样保留。官方打包、签名、测试与 Release 工作流不参与产品代码自动更新,因此 `GITHUB_TOKEN` 无需取得改写 Actions 工作流的权限。
|
||||
- 下游完整的 `.github/workflows/**` 目录和两个更新辅助脚本会作为稳定的本地引导层原样保留。官方打包、签名、测试与 Release 工作流不参与产品代码自动更新,因此 `GITHUB_TOKEN` 无需取得改写 Actions 工作流的权限。
|
||||
- 每次运行都会写 Job **Summary**:通道、目标版本、判定原因、是否 push。若 Summary 写明 *Already on upstream target* / 已对齐,绿色成功表示「已是目标版本」,不是静默故障。
|
||||
- 请优先用本工作流,而不是 GitHub **Sync fork**。Sync fork 跟的是上游 `main` 历史,可能让下一次 stable 运行合理变为 no-op。
|
||||
- 可选:仓库 Secret `EDGE_EVER_CLOUDFLARE_DEPLOY_HOOK_URL`,在成功 push 后触发 Cloudflare Deploy Hook(Git 集成偶发未构建时有用)。
|
||||
|
||||
@@ -2,7 +2,11 @@ import { execFileSync } from "node:child_process";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const protectedWorkflowPath = ".github/workflows";
|
||||
const protectedUpdaterPaths = [
|
||||
".github/workflows",
|
||||
"scripts/prepare-upstream-sync.mjs",
|
||||
"scripts/upstream-sync-plan.mjs",
|
||||
];
|
||||
|
||||
function runGit(arguments_, { allowFailure = false, cwd = process.cwd() } = {}) {
|
||||
try {
|
||||
@@ -35,40 +39,34 @@ function resolveCommit(cwd, revision) {
|
||||
return commit;
|
||||
}
|
||||
|
||||
function restoreProtectedWorkflows(cwd, baseCommit) {
|
||||
const workflowsExist = runGit(
|
||||
["cat-file", "-e", `${baseCommit}:${protectedWorkflowPath}`],
|
||||
{ allowFailure: true, cwd },
|
||||
);
|
||||
function restoreProtectedUpdater(cwd, baseCommit) {
|
||||
for (const path of protectedUpdaterPaths) {
|
||||
const pathExists = runGit(["cat-file", "-e", `${baseCommit}:${path}`], {
|
||||
allowFailure: true,
|
||||
cwd,
|
||||
});
|
||||
|
||||
if (workflowsExist.status === 0) {
|
||||
runGit(
|
||||
[
|
||||
"restore",
|
||||
`--source=${baseCommit}`,
|
||||
"--staged",
|
||||
"--worktree",
|
||||
"--",
|
||||
protectedWorkflowPath,
|
||||
],
|
||||
{ cwd },
|
||||
);
|
||||
return;
|
||||
if (pathExists.status === 0) {
|
||||
runGit(
|
||||
["restore", `--source=${baseCommit}`, "--staged", "--worktree", "--", path],
|
||||
{ cwd },
|
||||
);
|
||||
} else {
|
||||
runGit(["rm", "-r", "-f", "--ignore-unmatch", "--", path], { cwd });
|
||||
}
|
||||
}
|
||||
|
||||
runGit(["rm", "-r", "-f", "--ignore-unmatch", "--", protectedWorkflowPath], { cwd });
|
||||
}
|
||||
|
||||
function requireNoWorkflowChanges(cwd, baseCommit) {
|
||||
function requireNoUpdaterChanges(cwd, baseCommit) {
|
||||
const result = runGit(
|
||||
["diff", "--cached", "--quiet", baseCommit, "--", protectedWorkflowPath],
|
||||
["diff", "--cached", "--quiet", baseCommit, "--", ...protectedUpdaterPaths],
|
||||
{ allowFailure: true, cwd },
|
||||
);
|
||||
if (result.status === 1) {
|
||||
throw new Error(`${protectedWorkflowPath} must remain unchanged in deployment forks`);
|
||||
throw new Error("The downstream updater layer must remain unchanged in deployment forks");
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`Could not verify the protected workflow tree (git diff exited ${result.status})`);
|
||||
throw new Error(`Could not verify the protected updater layer (git diff exited ${result.status})`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +85,7 @@ function requirePreparedChanges(cwd, baseCommit) {
|
||||
|
||||
function prepareSnapshot(cwd, baseCommit, targetCommit) {
|
||||
runGit(["read-tree", "--reset", "-u", targetCommit], { cwd });
|
||||
restoreProtectedWorkflows(cwd, baseCommit);
|
||||
restoreProtectedUpdater(cwd, baseCommit);
|
||||
|
||||
const managedTreeMatches = runGit(
|
||||
[
|
||||
@@ -98,6 +96,8 @@ function prepareSnapshot(cwd, baseCommit, targetCommit) {
|
||||
"--",
|
||||
".",
|
||||
":(exclude).github/workflows/**",
|
||||
":(exclude)scripts/prepare-upstream-sync.mjs",
|
||||
":(exclude)scripts/upstream-sync-plan.mjs",
|
||||
],
|
||||
{ allowFailure: true, cwd },
|
||||
);
|
||||
@@ -178,7 +178,7 @@ function prepareCustomizedMerge(cwd, baseCommit, targetCommit) {
|
||||
);
|
||||
}
|
||||
|
||||
restoreProtectedWorkflows(cwd, baseCommit);
|
||||
restoreProtectedUpdater(cwd, baseCommit);
|
||||
const unresolved = runGit(["diff", "--name-only", "--diff-filter=U"], { cwd })
|
||||
.stdout.trim()
|
||||
.split("\n")
|
||||
@@ -227,7 +227,7 @@ export function prepareUpstreamSync({
|
||||
prepareCustomizedMerge(cwd, baseCommit, targetCommit);
|
||||
}
|
||||
|
||||
requireNoWorkflowChanges(cwd, baseCommit);
|
||||
requireNoUpdaterChanges(cwd, baseCommit);
|
||||
requirePreparedChanges(cwd, baseCommit);
|
||||
|
||||
return { alignMode, baseCommit, targetCommit };
|
||||
@@ -244,7 +244,7 @@ function runCli() {
|
||||
targetRevision: process.env.EDGE_SYNC_TARGET_COMMIT,
|
||||
});
|
||||
process.stdout.write(
|
||||
`[ok] prepared ${result.alignMode} update ${result.baseCommit.slice(0, 12)} -> ${result.targetCommit.slice(0, 12)} without changing ${protectedWorkflowPath}\n`,
|
||||
`[ok] prepared ${result.alignMode} update ${result.baseCommit.slice(0, 12)} -> ${result.targetCommit.slice(0, 12)} without changing the downstream updater layer\n`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -317,7 +317,9 @@ describe("Cloudflare deployment entrypoints", () => {
|
||||
expect(workflow).not.toContain("git reset --hard");
|
||||
expect(workflow).toContain("scripts/prepare-upstream-sync.mjs");
|
||||
expect(workflow).toContain("':(exclude).github/workflows/**'");
|
||||
expect(workflow).toContain("Refusing to publish an update that changes downstream");
|
||||
expect(workflow).toContain("planner_path");
|
||||
expect(workflow).toContain('if [ "${align_mode}" = "reset" ]');
|
||||
expect(workflow).toContain("Refusing to publish an update that changes the downstream updater");
|
||||
expect(workflow).toContain("content_matches_target");
|
||||
expect(workflow).toContain("already_on_target");
|
||||
expect(workflow).toContain("fork_mode=mirror");
|
||||
@@ -435,6 +437,16 @@ describe("Cloudflare deployment entrypoints", () => {
|
||||
".github/workflows/sync-edgeever-upstream.yml",
|
||||
"name: downstream base\n",
|
||||
);
|
||||
writeFixtureFile(
|
||||
workingDirectory,
|
||||
"scripts/prepare-upstream-sync.mjs",
|
||||
"// downstream prepare helper\n",
|
||||
);
|
||||
writeFixtureFile(
|
||||
workingDirectory,
|
||||
"scripts/upstream-sync-plan.mjs",
|
||||
"// downstream planner\n",
|
||||
);
|
||||
runFixtureGit(workingDirectory, "add", ".");
|
||||
runFixtureGit(workingDirectory, "commit", "-m", "base");
|
||||
|
||||
@@ -451,6 +463,16 @@ describe("Cloudflare deployment entrypoints", () => {
|
||||
".github/workflows/windows-test-signing.yml",
|
||||
"name: official only\n",
|
||||
);
|
||||
writeFixtureFile(
|
||||
workingDirectory,
|
||||
"scripts/prepare-upstream-sync.mjs",
|
||||
"// upstream prepare helper\n",
|
||||
);
|
||||
writeFixtureFile(
|
||||
workingDirectory,
|
||||
"scripts/upstream-sync-plan.mjs",
|
||||
"// upstream planner\n",
|
||||
);
|
||||
rmSync(resolve(workingDirectory, "removed.txt"));
|
||||
runFixtureGit(workingDirectory, "add", "-A");
|
||||
runFixtureGit(workingDirectory, "commit", "-m", "upstream target");
|
||||
@@ -478,6 +500,14 @@ describe("Cloudflare deployment entrypoints", () => {
|
||||
workingDirectory,
|
||||
".github/workflows/sync-edgeever-upstream.yml",
|
||||
), "utf8")).toBe("name: downstream updater\n");
|
||||
expect(readFileSync(resolve(
|
||||
workingDirectory,
|
||||
"scripts/prepare-upstream-sync.mjs",
|
||||
), "utf8")).toBe("// downstream prepare helper\n");
|
||||
expect(readFileSync(resolve(
|
||||
workingDirectory,
|
||||
"scripts/upstream-sync-plan.mjs",
|
||||
), "utf8")).toBe("// downstream planner\n");
|
||||
expect(readdirSync(resolve(workingDirectory, ".github/workflows"))).toEqual([
|
||||
"sync-edgeever-upstream.yml",
|
||||
]);
|
||||
@@ -488,6 +518,8 @@ describe("Cloudflare deployment entrypoints", () => {
|
||||
"--quiet",
|
||||
"--",
|
||||
".github/workflows",
|
||||
"scripts/prepare-upstream-sync.mjs",
|
||||
"scripts/upstream-sync-plan.mjs",
|
||||
)).toBe(0);
|
||||
|
||||
runFixtureGit(workingDirectory, "commit", "-m", "sync upstream product");
|
||||
@@ -504,6 +536,8 @@ describe("Cloudflare deployment entrypoints", () => {
|
||||
"--",
|
||||
".",
|
||||
":(exclude).github/workflows/**",
|
||||
":(exclude)scripts/prepare-upstream-sync.mjs",
|
||||
":(exclude)scripts/upstream-sync-plan.mjs",
|
||||
)).toBe(0);
|
||||
expect(fixtureGitStatus(
|
||||
workingDirectory,
|
||||
@@ -513,6 +547,8 @@ describe("Cloudflare deployment entrypoints", () => {
|
||||
"HEAD",
|
||||
"--",
|
||||
".github/workflows",
|
||||
"scripts/prepare-upstream-sync.mjs",
|
||||
"scripts/upstream-sync-plan.mjs",
|
||||
)).toBe(0);
|
||||
} finally {
|
||||
rmSync(workingDirectory, { force: true, recursive: true });
|
||||
|
||||
Reference in new issue
Block a user