Skip to content

Commit ff3334a

Browse files
mrjfCopilot
andauthored
harden(go-migration): require upstream APM freshness (#121)
* harden(go-migration): require upstream APM freshness * test(migration-ci): align workflow assertion with env-based enforcement checks Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com>
1 parent c27194e commit ff3334a

11 files changed

Lines changed: 1042 additions & 19 deletions

.crane/scripts/score.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ type CutoverGates struct {
6464
FunctionalContracts float64 `json:"functional_contracts"`
6565
StateDiffContracts float64 `json:"state_diff_contracts"`
6666
PythonBehaviorContracts float64 `json:"python_behavior_contracts"`
67+
UpstreamFreshness string `json:"upstream_freshness"`
68+
UpstreamContracts float64 `json:"upstream_contracts"`
6769
GoldenFixtureCorpus string `json:"golden_fixture_corpus"`
6870
AllGoGoldenTests string `json:"all_go_golden_tests"`
6971
NoPythonRuntime string `json:"no_python_runtime_dependency"`
@@ -104,6 +106,8 @@ type Score struct {
104106
PythonTestsPassing bool `json:"python_tests_passing"`
105107
GoTestsPassing bool `json:"go_tests_passing"`
106108
BenchmarksPassing bool `json:"benchmarks_passing"`
109+
UpstreamFreshness bool `json:"upstream_freshness"`
110+
UpstreamContracts float64 `json:"upstream_contracts"`
107111
GoldenFixtureCorpus bool `json:"golden_fixture_corpus"`
108112
AllGoGoldenTests bool `json:"all_go_golden_tests"`
109113
NoPythonRuntime bool `json:"no_python_runtime_dependency"`
@@ -152,6 +156,8 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
152156
functional := RatioGate{}
153157
stateDiff := RatioGate{}
154158
behaviorContracts := RatioGate{}
159+
upstreamFreshness := BoolGate{}
160+
upstreamContracts := RatioGate{}
155161
goldenFixtureCorpus := BoolGate{}
156162
allGoGoldenTests := BoolGate{}
157163
noPythonRuntime := BoolGate{}
@@ -172,6 +178,8 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
172178
&functional,
173179
&stateDiff,
174180
&behaviorContracts,
181+
&upstreamFreshness,
182+
&upstreamContracts,
175183
&goldenFixtureCorpus,
176184
&allGoGoldenTests,
177185
&noPythonRuntime,
@@ -199,6 +207,8 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
199207
&functional,
200208
&stateDiff,
201209
&behaviorContracts,
210+
&upstreamFreshness,
211+
&upstreamContracts,
202212
&goldenFixtureCorpus,
203213
&allGoGoldenTests,
204214
&noPythonRuntime,
@@ -283,6 +293,12 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
283293
if !behaviorContracts.Seen {
284294
behaviorContracts = missingRatioGate()
285295
}
296+
if !upstreamFreshness.Seen {
297+
upstreamFreshness = BoolGate{Seen: true, Passed: false}
298+
}
299+
if !upstreamContracts.Seen {
300+
upstreamContracts = missingRatioGate()
301+
}
286302
if !pythonTests.Seen {
287303
pythonTests = BoolGate{Seen: true, Passed: testPassed(passed, failed, "TestParityCompletionPythonSuite")}
288304
}
@@ -302,6 +318,8 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
302318
FunctionalContracts: functional.Percent(),
303319
StateDiffContracts: stateDiff.Percent(),
304320
PythonBehaviorContracts: behaviorContracts.Percent(),
321+
UpstreamFreshness: passFail(upstreamFreshness.OK()),
322+
UpstreamContracts: upstreamContracts.Percent(),
305323
GoldenFixtureCorpus: passFail(goldenFixtureCorpus.OK()),
306324
AllGoGoldenTests: passFail(allGoGoldenTests.OK()),
307325
NoPythonRuntime: passFail(noPythonRuntime.OK()),
@@ -328,6 +346,8 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
328346
gates.FunctionalContracts == 1.0 &&
329347
gates.StateDiffContracts == 1.0 &&
330348
gates.PythonBehaviorContracts == 1.0 &&
349+
gates.UpstreamFreshness == "pass" &&
350+
gates.UpstreamContracts == 1.0 &&
331351
gates.GoldenFixtureCorpus == "pass" &&
332352
gates.AllGoGoldenTests == "pass" &&
333353
gates.NoPythonRuntime == "pass" &&
@@ -372,6 +392,8 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
372392
PythonTestsPassing: gates.PythonTests == "pass",
373393
GoTestsPassing: gates.GoTests == "pass",
374394
BenchmarksPassing: gates.Benchmarks == "pass",
395+
UpstreamFreshness: gates.UpstreamFreshness == "pass",
396+
UpstreamContracts: gates.UpstreamContracts,
375397
GoldenFixtureCorpus: gates.GoldenFixtureCorpus == "pass",
376398
AllGoGoldenTests: gates.AllGoGoldenTests == "pass",
377399
NoPythonRuntime: gates.NoPythonRuntime == "pass",
@@ -405,6 +427,8 @@ func applyGateEvent(
405427
functional *RatioGate,
406428
stateDiff *RatioGate,
407429
behaviorContracts *RatioGate,
430+
upstreamFreshness *BoolGate,
431+
upstreamContracts *RatioGate,
408432
goldenFixtureCorpus *BoolGate,
409433
allGoGoldenTests *BoolGate,
410434
noPythonRuntime *BoolGate,
@@ -427,6 +451,10 @@ func applyGateEvent(
427451
*stateDiff = RatioGate{Seen: true, Passing: gate.Passing, Total: gate.Total}
428452
case "python_behavior_contracts":
429453
*behaviorContracts = RatioGate{Seen: true, Passing: gate.Passing, Total: gate.Total}
454+
case "upstream_freshness":
455+
*upstreamFreshness = BoolGate{Seen: true, Passed: gate.Passed}
456+
case "upstream_contracts":
457+
*upstreamContracts = RatioGate{Seen: true, Passing: gate.Passing, Total: gate.Total}
430458
case "golden_fixture_corpus":
431459
*goldenFixtureCorpus = BoolGate{Seen: true, Passed: gate.Passed}
432460
case "all_go_golden_tests":
@@ -485,6 +513,8 @@ func gateResults(gates CutoverGates) []GateResult {
485513
{Name: "functional_contracts", Passing: gates.FunctionalContracts == 1.0},
486514
{Name: "state_diff_contracts", Passing: gates.StateDiffContracts == 1.0},
487515
{Name: "python_behavior_contracts", Passing: gates.PythonBehaviorContracts == 1.0},
516+
{Name: "upstream_freshness", Passing: gates.UpstreamFreshness == "pass"},
517+
{Name: "upstream_contracts", Passing: gates.UpstreamContracts == 1.0},
488518
{Name: "golden_fixture_corpus", Passing: gates.GoldenFixtureCorpus == "pass"},
489519
{Name: "all_go_golden_tests", Passing: gates.AllGoGoldenTests == "pass"},
490520
{Name: "no_python_runtime_dependency", Passing: gates.NoPythonRuntime == "pass"},

.github/workflows/migration-ci.yml

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ jobs:
6464
runs-on: ubuntu-24.04
6565
steps:
6666
- uses: actions/checkout@v4
67+
with:
68+
fetch-depth: 0
6769

6870
- uses: actions/setup-python@v5
6971
with:
@@ -92,12 +94,16 @@ jobs:
9294
9395
- name: Run CLI-agnostic Python behavior tests
9496
shell: bash
97+
env:
98+
EVENT_NAME: ${{ github.event_name }}
99+
ENFORCE_COMPLETION_INPUT: ${{ inputs.enforce_completion }}
100+
HEAD_REF: ${{ github.event.pull_request.head.ref }}
95101
run: |
96102
go build -o "$RUNNER_TEMP/apm-go" ./cmd/apm
97103
enforce_behavior_contracts=false
98-
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.enforce_completion == true }}" = "true" ]; then
104+
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "${ENFORCE_COMPLETION_INPUT:-false}" = "true" ]; then
99105
enforce_behavior_contracts=true
100-
elif [ "${{ github.event_name }}" = "pull_request" ] && [[ "${{ github.event.pull_request.head.ref }}" == crane/* ]]; then
106+
elif [ "$EVENT_NAME" = "pull_request" ] && [[ "${HEAD_REF:-}" == crane/* ]]; then
101107
enforce_behavior_contracts=true
102108
fi
103109
if [ "$enforce_behavior_contracts" = "true" ]; then
@@ -113,11 +119,15 @@ jobs:
113119
114120
- name: Run Go parity tests
115121
shell: bash
122+
env:
123+
EVENT_NAME: ${{ github.event_name }}
124+
ENFORCE_COMPLETION_INPUT: ${{ inputs.enforce_completion }}
125+
HEAD_REF: ${{ github.event.pull_request.head.ref }}
116126
run: |
117127
enforce_completion=false
118-
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.enforce_completion == true }}" = "true" ]; then
128+
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "${ENFORCE_COMPLETION_INPUT:-false}" = "true" ]; then
119129
enforce_completion=true
120-
elif [ "${{ github.event_name }}" = "pull_request" ] && [[ "${{ github.event.pull_request.head.ref }}" == crane/* ]]; then
130+
elif [ "$EVENT_NAME" = "pull_request" ] && [[ "${HEAD_REF:-}" == crane/* ]]; then
121131
enforce_completion=true
122132
fi
123133
@@ -136,17 +146,43 @@ jobs:
136146
shell: bash
137147
run: |
138148
set +e
139-
APM_PYTHON_BIN= \
140-
APM_PYTHON_CONTRACT_INVENTORY= \
141-
PYTHONPATH= \
142-
VIRTUAL_ENV= \
149+
APM_PYTHON_BIN="" \
150+
APM_PYTHON_CONTRACT_INVENTORY="" \
151+
PYTHONPATH="" \
152+
VIRTUAL_ENV="" \
143153
go test -json ./cmd/apm -run '^TestGoCutover' \
144154
| tee "$RUNNER_TEMP/go-cutover-events.json"
145155
status=${PIPESTATUS[0]}
146156
set -e
147157
cat "$RUNNER_TEMP/go-cutover-events.json" >> "$RUNNER_TEMP/go-test-events.json"
148158
echo "GO_CUTOVER_STATUS=$status" >> "$GITHUB_ENV"
149159
160+
- name: Check upstream APM contract coverage
161+
shell: bash
162+
run: |
163+
git remote add upstream https://github.com/microsoft/apm.git 2>/dev/null || \
164+
git remote set-url upstream https://github.com/microsoft/apm.git
165+
git fetch upstream main --prune
166+
167+
upstream_args=(
168+
--upstream-ref upstream/main
169+
--head-ref HEAD
170+
--coverage tests/parity/upstream_contract_coverage.yml
171+
--summary "$RUNNER_TEMP/upstream-apm-contracts.md"
172+
)
173+
if [ "${MIGRATION_COMPLETION_ENFORCED:-false}" = "true" ]; then
174+
upstream_args+=(--enforce)
175+
fi
176+
177+
set +e
178+
uv run python scripts/ci/upstream_apm_contracts.py check \
179+
"${upstream_args[@]}" \
180+
| tee "$RUNNER_TEMP/upstream-apm-contracts.txt"
181+
status=${PIPESTATUS[0]}
182+
set -e
183+
cat "$RUNNER_TEMP/upstream-apm-contracts.txt" >> "$RUNNER_TEMP/go-test-events.json"
184+
echo "UPSTREAM_APM_STATUS=$status" >> "$GITHUB_ENV"
185+
150186
- name: Compute migration score
151187
run: |
152188
go run .crane/scripts/score.go < "$RUNNER_TEMP/go-test-events.json" | tee "$RUNNER_TEMP/migration-score.json"
@@ -191,6 +227,7 @@ jobs:
191227
test "${PYTHON_CLI_CONTRACT_STATUS:-1}" = "0"
192228
test "${GO_TEST_STATUS:-1}" = "0"
193229
test "${GO_CUTOVER_STATUS:-1}" = "0"
230+
test "${UPSTREAM_APM_STATUS:-1}" = "0"
194231
else
195232
if [ "${PYTHON_CLI_CONTRACT_STATUS:-1}" != "0" ]; then
196233
echo "::notice::Python behavior contract tests are incomplete in collection mode."
@@ -201,6 +238,9 @@ jobs:
201238
if [ "${GO_CUTOVER_STATUS:-1}" != "0" ]; then
202239
echo "::notice::Go-only cutover gate is incomplete in collection mode."
203240
fi
241+
if [ "${UPSTREAM_APM_STATUS:-1}" != "0" ]; then
242+
echo "::notice::Upstream APM freshness/contract coverage is incomplete in collection mode."
243+
fi
204244
fi
205245
206246
- name: Upload parity evidence
@@ -215,6 +255,8 @@ jobs:
215255
${{ runner.temp }}/python-behavior-contracts.json
216256
${{ runner.temp }}/python-contract-coverage.md
217257
${{ runner.temp }}/python-cli-contract-tests.txt
258+
${{ runner.temp }}/upstream-apm-contracts.txt
259+
${{ runner.temp }}/upstream-apm-contracts.md
218260
if-no-files-found: ignore
219261
retention-days: 14
220262

@@ -247,11 +289,15 @@ jobs:
247289

248290
- name: Run Python-vs-Go CLI benchmark
249291
shell: bash
292+
env:
293+
EVENT_NAME: ${{ github.event_name }}
294+
ENFORCE_COMPLETION_INPUT: ${{ inputs.enforce_completion }}
295+
HEAD_REF: ${{ github.event.pull_request.head.ref }}
250296
run: |
251297
enforce_completion=false
252-
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.enforce_completion == true }}" = "true" ]; then
298+
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "${ENFORCE_COMPLETION_INPUT:-false}" = "true" ]; then
253299
enforce_completion=true
254-
elif [ "${{ github.event_name }}" = "pull_request" ] && [[ "${{ github.event.pull_request.head.ref }}" == crane/* ]]; then
300+
elif [ "$EVENT_NAME" = "pull_request" ] && [[ "${HEAD_REF:-}" == crane/* ]]; then
255301
enforce_completion=true
256302
fi
257303
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
name: Upstream APM Sync
2+
3+
on:
4+
schedule:
5+
- cron: "17 * * * *"
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: write
10+
pull-requests: write
11+
issues: write
12+
13+
concurrency:
14+
group: upstream-apm-sync
15+
cancel-in-progress: false
16+
17+
env:
18+
UPSTREAM_REPO: https://github.com/microsoft/apm.git
19+
UPSTREAM_BRANCH: main
20+
SYNC_BRANCH: automation/upstream-microsoft-apm-main
21+
22+
jobs:
23+
sync:
24+
name: Sync microsoft/apm main
25+
runs-on: ubuntu-24.04
26+
steps:
27+
- name: Check out main
28+
uses: actions/checkout@v4
29+
with:
30+
ref: main
31+
fetch-depth: 0
32+
33+
- name: Configure git identity
34+
run: |
35+
git config user.name "github-actions[bot]"
36+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
37+
38+
- name: Fetch upstream
39+
run: |
40+
git remote add upstream "$UPSTREAM_REPO" 2>/dev/null || \
41+
git remote set-url upstream "$UPSTREAM_REPO"
42+
git fetch upstream "$UPSTREAM_BRANCH" --prune
43+
git fetch origin main --prune
44+
45+
- name: Merge upstream into sync branch
46+
id: merge
47+
shell: bash
48+
run: |
49+
upstream_ref="upstream/${UPSTREAM_BRANCH}"
50+
upstream_sha="$(git rev-parse "$upstream_ref")"
51+
origin_sha="$(git rev-parse origin/main)"
52+
echo "upstream_sha=$upstream_sha" >> "$GITHUB_OUTPUT"
53+
echo "origin_sha=$origin_sha" >> "$GITHUB_OUTPUT"
54+
55+
if git merge-base --is-ancestor "$upstream_ref" origin/main; then
56+
echo "changed=false" >> "$GITHUB_OUTPUT"
57+
echo "origin/main already contains ${upstream_ref} (${upstream_sha})."
58+
exit 0
59+
fi
60+
61+
git switch --force-create "$SYNC_BRANCH" origin/main
62+
if ! git merge --no-ff --no-edit "$upstream_ref"; then
63+
git status --short
64+
echo "::error::Automatic upstream merge conflicted. Resolve manually by merging ${upstream_ref} into main."
65+
exit 1
66+
fi
67+
git push --force-with-lease origin "$SYNC_BRANCH"
68+
echo "changed=true" >> "$GITHUB_OUTPUT"
69+
70+
- name: Create or update sync PR
71+
if: steps.merge.outputs.changed == 'true'
72+
id: pr
73+
env:
74+
GH_TOKEN: ${{ github.token }}
75+
shell: bash
76+
run: |
77+
body="$RUNNER_TEMP/upstream-sync-pr.md"
78+
cat > "$body" <<EOF
79+
## TL;DR
80+
81+
This PR merges \`microsoft/apm@${{ steps.merge.outputs.upstream_sha }}\` into \`githubnext/apm@main\`.
82+
83+
## Required follow-up before Go migration completion
84+
85+
- Review the upstream Python diff for new CLI/runtime behavior.
86+
- Add or update real Go behavior tests for every changed Python source/test contract.
87+
- Advance \`tests/parity/upstream_contract_coverage.yml\` with a reviewed range from the previous upstream SHA to \`${{ steps.merge.outputs.upstream_sha }}\`.
88+
- Run the enforcing migration gate before declaring issue #78 complete.
89+
90+
This PR is created by \`.github/workflows/upstream-apm-sync.yml\`.
91+
EOF
92+
93+
pr_number="$(gh pr list \
94+
--head "$SYNC_BRANCH" \
95+
--base main \
96+
--state open \
97+
--json number \
98+
--jq '.[0].number')"
99+
100+
if [ -z "$pr_number" ]; then
101+
pr_url="$(gh pr create \
102+
--base main \
103+
--head "$SYNC_BRANCH" \
104+
--title "chore(upstream): merge microsoft/apm main" \
105+
--body-file "$body")"
106+
pr_number="${pr_url##*/}"
107+
else
108+
gh pr edit "$pr_number" --body-file "$body"
109+
fi
110+
111+
echo "number=$pr_number" >> "$GITHUB_OUTPUT"
112+
gh pr view "$pr_number" --json url --jq .url
113+
114+
- name: Request merge-commit auto-merge
115+
if: steps.merge.outputs.changed == 'true'
116+
env:
117+
GH_TOKEN: ${{ github.token }}
118+
run: |
119+
if gh pr merge "${{ steps.pr.outputs.number }}" --auto --merge --delete-branch; then
120+
echo "Auto-merge requested for upstream sync PR #${{ steps.pr.outputs.number }}."
121+
else
122+
echo "::warning::Could not enable auto-merge. PR #${{ steps.pr.outputs.number }} is ready for maintainer review/merge."
123+
fi
124+
125+
- name: Summarize
126+
if: always()
127+
run: |
128+
{
129+
echo "## Upstream APM Sync"
130+
echo
131+
echo "- Upstream: \`${UPSTREAM_REPO}\`"
132+
echo "- Branch: \`${UPSTREAM_BRANCH}\`"
133+
echo "- Sync branch: \`${SYNC_BRANCH}\`"
134+
echo "- Changed: \`${{ steps.merge.outputs.changed || 'unknown' }}\`"
135+
echo "- Upstream SHA: \`${{ steps.merge.outputs.upstream_sha || 'unknown' }}\`"
136+
echo "- Origin SHA: \`${{ steps.merge.outputs.origin_sha || 'unknown' }}\`"
137+
} >> "$GITHUB_STEP_SUMMARY"

0 commit comments

Comments
 (0)