Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
run: bash scripts/automation/cli_publication.sh check-build

repository:
name: Hermetic repository contract
name: impl-py checks
runs-on: ubuntu-latest
services:
postgres:
Expand Down Expand Up @@ -101,11 +101,11 @@ jobs:
if: github.event_name == 'pull_request'
run: pdm run check:migration-history "origin/${{ github.base_ref }}"

- name: Run pre-commit contract
run: pdm run pre-commit run --all-files
- name: Validate GitHub Actions definitions
run: pdm run pre-commit run actionlint --all-files

artifact:
name: Portable peer database runtime
name: impl-py e2e tests
runs-on: ubuntu-latest
services:
postgres:
Expand Down
28 changes: 26 additions & 2 deletions .github/workflows/preview-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,35 @@ concurrency:
cancel-in-progress: true

jobs:
deploy:
scope:
name: impl-py preview checks
if: >-
github.event.action != 'closed' &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
outputs:
required: ${{ steps.scope.outputs.required }}
steps:
- name: Checkout trusted preview scope controller
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
ref: ${{ github.workflow_sha }}
persist-credentials: false

- name: Check the complete exact-head pull request diff
id: scope
env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: bash scripts/automation/preview_workflow.sh application-required

deploy:
name: impl-py preview
needs: scope
if: needs.scope.outputs.required == 'true'
runs-on: ubuntu-latest
environment: preview
steps:
- name: Checkout trusted delivery implementation
Expand Down Expand Up @@ -128,7 +152,7 @@ jobs:
run: bash .delivery/scripts/automation/preview_workflow.sh summarize

cleanup:
name: Retire preview resources
name: impl-py preview cleanup
if: >-
github.event.action == 'closed' &&
github.event.pull_request.head.repo.full_name == github.repository
Expand Down
9 changes: 9 additions & 0 deletions docs/40-deployment/heroku.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ Trusted same-repository pull requests currently use:
- matching seven-day Neon branch `preview/core-py/pr-<number>`;
- no Heroku addon.

应用 Preview 在读取完整 PR 文件清单后,仅对全部变更都位于根目录 `tests/` 的候选跳过 wheel、
镜像、Registry 和 Heroku 交付。重命名同时检查新旧路径;文件清单不完整、读取失败或查询前后
head/base 变化时,继续原交付流程。不将此规则扩展到 `scripts/tests/`,因为镜像包含整个
`scripts/`。判定来自受信 controller,不执行候选代码。

Neon 分支仍由原必需检查创建或复用;它的成功状态继续代表实际供给,不表示一次无资源的跳过。
打开的 PR 已有现场不会因后续只剩测试改动而删除;关闭时的 Registry、Heroku 和 Neon 清理
独立于路径判定,始终按原身份边界执行。此策略减少应用交付,不承诺纯测试 PR 零资源成本。

The branch workflow owns only exact isolated branch creation/deletion. After the repository,
portable-runtime, and branch checks pass for the exact PR SHA, preview delivery:

Expand Down
4 changes: 2 additions & 2 deletions scripts/automation/github_delivery.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ case "${1:-}" in
test "$(jq -r '.head.sha' <<<"$pr_json")" = "$HEAD_SHA"
test "$(jq -r '.state' <<<"$pr_json")" = "open"
required_checks=(
"Hermetic repository contract"
"Portable peer database runtime"
"impl-py checks"
"impl-py e2e tests"
"Provision isolated branch"
)
;;
Expand Down
3 changes: 0 additions & 3 deletions scripts/automation/preview_delivery.sh
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,9 @@ case "${1:-}" in
test "$stage" = review
fi
done
deployed_release="$(heroku releases --app "$app_name" --json | jq -r \
'[.[] | select(.status == "succeeded" and (.description | startswith("Deployed web")))] | first | .version // empty')"
postgrest_release="$(heroku releases --app "$postgrest_app_name" --json | jq -r \
'[.[] | select(.status == "succeeded" and (.description | startswith("Deployed web")))] | first | .version // empty')"
emit_output app_name "$app_name"
emit_output has_deployed_release "$([ -n "$deployed_release" ] && echo true || echo false)"
emit_output has_postgrest_release "$([ -n "$postgrest_release" ] && echo true || echo false)"
emit_output postgrest_app_name "$postgrest_app_name"
emit_output postgrest_url "$(jq -r '.app.web_url' <<<"$postgrest_json")"
Expand Down
39 changes: 38 additions & 1 deletion scripts/automation/preview_workflow.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,43 @@ validate_preview_identity() {
}

case "${1:-}" in
application-required)
for name in GH_TOKEN GITHUB_REPOSITORY PR_NUMBER HEAD_SHA BASE_SHA; do require_env "$name"; done
[[ "$PR_NUMBER" =~ ^[0-9]+$ ]]
[[ "$HEAD_SHA" =~ ^[a-f0-9]{40}$ && "$BASE_SHA" =~ ^[a-f0-9]{40}$ ]]
required=true
# Only a complete, stable PR diff may suppress delivery; uncertainty keeps the existing path.
endpoint="repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER"
if before="$(gh api "$endpoint")" &&
files="$(gh api --paginate --slurp "$endpoint/files?per_page=100" |
jq '[.[][] | {filename, previous_filename}]')" &&
after="$(gh api "$endpoint")" &&
printf '%s\n' "$before" "$files" "$after" |
jq -se --arg head "$HEAD_SHA" --arg base "$BASE_SHA" --arg repo "$GITHUB_REPOSITORY" '
.[0] as $before | .[1] as $files | .[2] as $after |
all([$before, $after][];
.state == "open" and .head.sha == $head and .base.sha == $base and
.head.repo.full_name == $repo and .base.repo.full_name == $repo and
.base.ref == "main") and
$before.changed_files > 0 and
$before.changed_files == $after.changed_files and
($files | length) == $before.changed_files and
all($files[];
(.filename | startswith("tests/")) and
((.previous_filename // .filename) | startswith("tests/")))
' >/dev/null; then
required=false
fi
emit_output required "$required"
if [ "$required" = false ]; then
printf '%s\n' '### 无需应用 Preview' '' \
'本次完整 PR 差异仅涉及根 tests/;跳过 wheel、镜像、Registry 和 Heroku 交付。' \
'Neon 必需检查照常运行;已有现场不删除,PR 关闭清理不受影响。' | append_summary
else
printf '%s\n' '### 继续应用 Preview' '' \
'差异涉及运行产物,或无法确认完整的纯 tests/ 差异;保留既有交付与必需检查。' | append_summary
fi
;;
build-extensions)
validate_preview_identity
preview_inputs="$GITHUB_WORKSPACE/.extension-preview-inputs"
Expand Down Expand Up @@ -61,5 +98,5 @@ EOF
fi
done
;;
*) echo "usage: $0 build-extensions|build-images|summarize|validate-identity|cleanup-apps" >&2; exit 2 ;;
*) echo "usage: $0 application-required|build-extensions|build-images|summarize|validate-identity|cleanup-apps" >&2; exit 2 ;;
esac
70 changes: 70 additions & 0 deletions scripts/tests/test_preview_delivery.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from pathlib import Path
import subprocess

import pytest


ROOT = Path(__file__).resolve().parents[2]

Expand Down Expand Up @@ -96,3 +98,71 @@ def test_failed_migration_keeps_observed_heads_and_reports_partial_delivery(tmp_
assert '"version":7' in summary
assert "https://snapshot.pages.dev" in summary
assert "secret-not-for-report" not in summary + failed.stdout + failed.stderr


@pytest.mark.parametrize(
"paths,changed_files,scenario,required",
[
(["tests/a.py", "tests/b.py"], 2, None, "false"),
(["tests/a.py", "app/main.py"], 2, None, "true"),
(["tests/a.py"], 1, "rename-from-app", "true"),
(["scripts/tests/a.py"], 1, None, "true"),
(["tests/a.py"], 3001, None, "true"),
(["tests/a.py"], 1, "head", "true"),
(["tests/a.py"], 1, "base", "true"),
(["tests/a.py"], 1, "api-failure", "true"),
([], 0, None, "true"),
],
)
def test_preview_skips_only_a_complete_stable_tests_only_diff(
tmp_path, paths, changed_files, scenario, required
):
pull = {
"state": "open",
"head": {"sha": "a" * 40, "repo": {"full_name": "InKCre/impl-py"}},
"base": {
"sha": "b" * 40,
"ref": "main",
"repo": {"full_name": "InKCre/impl-py"},
},
"changed_files": changed_files,
}
(tmp_path / "before.json").write_text(json.dumps(pull))
if scenario in ("head", "base"):
pull[scenario]["sha"] = "c" * 40
(tmp_path / "after.json").write_text(json.dumps(pull))
pages = [[{"filename": path}] for path in paths]
if scenario == "rename-from-app":
pages[0][0]["previous_filename"] = "app/main.py"
(tmp_path / "files.json").write_text(json.dumps(pages))
binary = tmp_path / "gh"
binary.write_text(
"#!/bin/sh\n"
'cd "$GH_SCOPE_FIXTURE" || exit 1\n'
'case "$*" in\n'
' *"/files?"*) [ "$GH_SCOPE_FAILURE" = false ] || exit 17; cat files.json ;;\n'
" *) if [ -e called ]; then cat after.json; "
"else touch called; cat before.json; fi ;;\n"
"esac\n"
)
binary.chmod(0o700)
output = tmp_path / "output"
result = _run(
"preview_workflow.sh",
"application-required",
{
**os.environ,
"PATH": f"{tmp_path}:{os.environ['PATH']}",
"GH_SCOPE_FIXTURE": str(tmp_path),
"GH_SCOPE_FAILURE": str(scenario == "api-failure").lower(),
"GH_TOKEN": "fixture-only",
"GITHUB_REPOSITORY": "InKCre/impl-py",
"GITHUB_OUTPUT": str(output),
"GITHUB_STEP_SUMMARY": str(tmp_path / "summary"),
"HEAD_SHA": "a" * 40,
"BASE_SHA": "b" * 40,
"PR_NUMBER": "123",
},
)
assert result.returncode == 0, result.stderr
assert output.read_text() == f"required={required}\n"
Loading