Skip to content
Open
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
30 changes: 30 additions & 0 deletions .crane/scripts/score.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ type CutoverGates struct {
FunctionalContracts float64 `json:"functional_contracts"`
StateDiffContracts float64 `json:"state_diff_contracts"`
PythonBehaviorContracts float64 `json:"python_behavior_contracts"`
UpstreamFreshness string `json:"upstream_freshness"`
UpstreamContracts float64 `json:"upstream_contracts"`
GoldenFixtureCorpus string `json:"golden_fixture_corpus"`
AllGoGoldenTests string `json:"all_go_golden_tests"`
NoPythonRuntime string `json:"no_python_runtime_dependency"`
Expand Down Expand Up @@ -104,6 +106,8 @@ type Score struct {
PythonTestsPassing bool `json:"python_tests_passing"`
GoTestsPassing bool `json:"go_tests_passing"`
BenchmarksPassing bool `json:"benchmarks_passing"`
UpstreamFreshness bool `json:"upstream_freshness"`
UpstreamContracts float64 `json:"upstream_contracts"`
GoldenFixtureCorpus bool `json:"golden_fixture_corpus"`
AllGoGoldenTests bool `json:"all_go_golden_tests"`
NoPythonRuntime bool `json:"no_python_runtime_dependency"`
Expand Down Expand Up @@ -152,6 +156,8 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
functional := RatioGate{}
stateDiff := RatioGate{}
behaviorContracts := RatioGate{}
upstreamFreshness := BoolGate{}
upstreamContracts := RatioGate{}
goldenFixtureCorpus := BoolGate{}
allGoGoldenTests := BoolGate{}
noPythonRuntime := BoolGate{}
Expand All @@ -172,6 +178,8 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
&functional,
&stateDiff,
&behaviorContracts,
&upstreamFreshness,
&upstreamContracts,
&goldenFixtureCorpus,
&allGoGoldenTests,
&noPythonRuntime,
Expand Down Expand Up @@ -199,6 +207,8 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
&functional,
&stateDiff,
&behaviorContracts,
&upstreamFreshness,
&upstreamContracts,
&goldenFixtureCorpus,
&allGoGoldenTests,
&noPythonRuntime,
Expand Down Expand Up @@ -283,6 +293,12 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
if !behaviorContracts.Seen {
behaviorContracts = missingRatioGate()
}
if !upstreamFreshness.Seen {
upstreamFreshness = BoolGate{Seen: true, Passed: false}
}
if !upstreamContracts.Seen {
upstreamContracts = missingRatioGate()
}
if !pythonTests.Seen {
pythonTests = BoolGate{Seen: true, Passed: testPassed(passed, failed, "TestParityCompletionPythonSuite")}
}
Expand All @@ -302,6 +318,8 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
FunctionalContracts: functional.Percent(),
StateDiffContracts: stateDiff.Percent(),
PythonBehaviorContracts: behaviorContracts.Percent(),
UpstreamFreshness: passFail(upstreamFreshness.OK()),
UpstreamContracts: upstreamContracts.Percent(),
GoldenFixtureCorpus: passFail(goldenFixtureCorpus.OK()),
AllGoGoldenTests: passFail(allGoGoldenTests.OK()),
NoPythonRuntime: passFail(noPythonRuntime.OK()),
Expand All @@ -328,6 +346,8 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
gates.FunctionalContracts == 1.0 &&
gates.StateDiffContracts == 1.0 &&
gates.PythonBehaviorContracts == 1.0 &&
gates.UpstreamFreshness == "pass" &&
gates.UpstreamContracts == 1.0 &&
gates.GoldenFixtureCorpus == "pass" &&
gates.AllGoGoldenTests == "pass" &&
gates.NoPythonRuntime == "pass" &&
Expand Down Expand Up @@ -372,6 +392,8 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
PythonTestsPassing: gates.PythonTests == "pass",
GoTestsPassing: gates.GoTests == "pass",
BenchmarksPassing: gates.Benchmarks == "pass",
UpstreamFreshness: gates.UpstreamFreshness == "pass",
UpstreamContracts: gates.UpstreamContracts,
GoldenFixtureCorpus: gates.GoldenFixtureCorpus == "pass",
AllGoGoldenTests: gates.AllGoGoldenTests == "pass",
NoPythonRuntime: gates.NoPythonRuntime == "pass",
Expand Down Expand Up @@ -405,6 +427,8 @@ func applyGateEvent(
functional *RatioGate,
stateDiff *RatioGate,
behaviorContracts *RatioGate,
upstreamFreshness *BoolGate,
upstreamContracts *RatioGate,
goldenFixtureCorpus *BoolGate,
allGoGoldenTests *BoolGate,
noPythonRuntime *BoolGate,
Expand All @@ -427,6 +451,10 @@ func applyGateEvent(
*stateDiff = RatioGate{Seen: true, Passing: gate.Passing, Total: gate.Total}
case "python_behavior_contracts":
*behaviorContracts = RatioGate{Seen: true, Passing: gate.Passing, Total: gate.Total}
case "upstream_freshness":
*upstreamFreshness = BoolGate{Seen: true, Passed: gate.Passed}
case "upstream_contracts":
*upstreamContracts = RatioGate{Seen: true, Passing: gate.Passing, Total: gate.Total}
case "golden_fixture_corpus":
*goldenFixtureCorpus = BoolGate{Seen: true, Passed: gate.Passed}
case "all_go_golden_tests":
Expand Down Expand Up @@ -485,6 +513,8 @@ func gateResults(gates CutoverGates) []GateResult {
{Name: "functional_contracts", Passing: gates.FunctionalContracts == 1.0},
{Name: "state_diff_contracts", Passing: gates.StateDiffContracts == 1.0},
{Name: "python_behavior_contracts", Passing: gates.PythonBehaviorContracts == 1.0},
{Name: "upstream_freshness", Passing: gates.UpstreamFreshness == "pass"},
{Name: "upstream_contracts", Passing: gates.UpstreamContracts == 1.0},
{Name: "golden_fixture_corpus", Passing: gates.GoldenFixtureCorpus == "pass"},
{Name: "all_go_golden_tests", Passing: gates.AllGoGoldenTests == "pass"},
{Name: "no_python_runtime_dependency", Passing: gates.NoPythonRuntime == "pass"},
Expand Down
66 changes: 56 additions & 10 deletions .github/workflows/migration-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ jobs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-python@v5
with:
Expand Down Expand Up @@ -92,12 +94,16 @@ jobs:

- name: Run CLI-agnostic Python behavior tests
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
ENFORCE_COMPLETION_INPUT: ${{ inputs.enforce_completion }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
go build -o "$RUNNER_TEMP/apm-go" ./cmd/apm
enforce_behavior_contracts=false
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.enforce_completion == true }}" = "true" ]; then
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "${ENFORCE_COMPLETION_INPUT:-false}" = "true" ]; then
enforce_behavior_contracts=true
elif [ "${{ github.event_name }}" = "pull_request" ] && [[ "${{ github.event.pull_request.head.ref }}" == crane/* ]]; then
elif [ "$EVENT_NAME" = "pull_request" ] && [[ "${HEAD_REF:-}" == crane/* ]]; then
enforce_behavior_contracts=true
fi
if [ "$enforce_behavior_contracts" = "true" ]; then
Expand All @@ -113,11 +119,15 @@ jobs:

- name: Run Go parity tests
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
ENFORCE_COMPLETION_INPUT: ${{ inputs.enforce_completion }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
enforce_completion=false
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.enforce_completion == true }}" = "true" ]; then
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "${ENFORCE_COMPLETION_INPUT:-false}" = "true" ]; then
enforce_completion=true
elif [ "${{ github.event_name }}" = "pull_request" ] && [[ "${{ github.event.pull_request.head.ref }}" == crane/* ]]; then
elif [ "$EVENT_NAME" = "pull_request" ] && [[ "${HEAD_REF:-}" == crane/* ]]; then
enforce_completion=true
fi

Expand All @@ -136,17 +146,43 @@ jobs:
shell: bash
run: |
set +e
APM_PYTHON_BIN= \
APM_PYTHON_CONTRACT_INVENTORY= \
PYTHONPATH= \
VIRTUAL_ENV= \
APM_PYTHON_BIN="" \
APM_PYTHON_CONTRACT_INVENTORY="" \
PYTHONPATH="" \
VIRTUAL_ENV="" \
go test -json ./cmd/apm -run '^TestGoCutover' \
| tee "$RUNNER_TEMP/go-cutover-events.json"
status=${PIPESTATUS[0]}
set -e
cat "$RUNNER_TEMP/go-cutover-events.json" >> "$RUNNER_TEMP/go-test-events.json"
echo "GO_CUTOVER_STATUS=$status" >> "$GITHUB_ENV"

- name: Check upstream APM contract coverage
shell: bash
run: |
git remote add upstream https://github.com/microsoft/apm.git 2>/dev/null || \
git remote set-url upstream https://github.com/microsoft/apm.git
git fetch upstream main --prune

upstream_args=(
--upstream-ref upstream/main
--head-ref HEAD
--coverage tests/parity/upstream_contract_coverage.yml
--summary "$RUNNER_TEMP/upstream-apm-contracts.md"
)
if [ "${MIGRATION_COMPLETION_ENFORCED:-false}" = "true" ]; then
upstream_args+=(--enforce)
fi

set +e
uv run python scripts/ci/upstream_apm_contracts.py check \
"${upstream_args[@]}" \
| tee "$RUNNER_TEMP/upstream-apm-contracts.txt"
status=${PIPESTATUS[0]}
set -e
cat "$RUNNER_TEMP/upstream-apm-contracts.txt" >> "$RUNNER_TEMP/go-test-events.json"
echo "UPSTREAM_APM_STATUS=$status" >> "$GITHUB_ENV"

- name: Compute migration score
run: |
go run .crane/scripts/score.go < "$RUNNER_TEMP/go-test-events.json" | tee "$RUNNER_TEMP/migration-score.json"
Expand Down Expand Up @@ -191,6 +227,7 @@ jobs:
test "${PYTHON_CLI_CONTRACT_STATUS:-1}" = "0"
test "${GO_TEST_STATUS:-1}" = "0"
test "${GO_CUTOVER_STATUS:-1}" = "0"
test "${UPSTREAM_APM_STATUS:-1}" = "0"
else
if [ "${PYTHON_CLI_CONTRACT_STATUS:-1}" != "0" ]; then
echo "::notice::Python behavior contract tests are incomplete in collection mode."
Expand All @@ -201,6 +238,9 @@ jobs:
if [ "${GO_CUTOVER_STATUS:-1}" != "0" ]; then
echo "::notice::Go-only cutover gate is incomplete in collection mode."
fi
if [ "${UPSTREAM_APM_STATUS:-1}" != "0" ]; then
echo "::notice::Upstream APM freshness/contract coverage is incomplete in collection mode."
fi
fi

- name: Upload parity evidence
Expand All @@ -215,6 +255,8 @@ jobs:
${{ runner.temp }}/python-behavior-contracts.json
${{ runner.temp }}/python-contract-coverage.md
${{ runner.temp }}/python-cli-contract-tests.txt
${{ runner.temp }}/upstream-apm-contracts.txt
${{ runner.temp }}/upstream-apm-contracts.md
if-no-files-found: ignore
retention-days: 14

Expand Down Expand Up @@ -247,11 +289,15 @@ jobs:

- name: Run Python-vs-Go CLI benchmark
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
ENFORCE_COMPLETION_INPUT: ${{ inputs.enforce_completion }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
enforce_completion=false
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.enforce_completion == true }}" = "true" ]; then
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "${ENFORCE_COMPLETION_INPUT:-false}" = "true" ]; then
enforce_completion=true
elif [ "${{ github.event_name }}" = "pull_request" ] && [[ "${{ github.event.pull_request.head.ref }}" == crane/* ]]; then
elif [ "$EVENT_NAME" = "pull_request" ] && [[ "${HEAD_REF:-}" == crane/* ]]; then
enforce_completion=true
fi

Expand Down
137 changes: 137 additions & 0 deletions .github/workflows/upstream-apm-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
name: Upstream APM Sync

on:
schedule:
- cron: "17 * * * *"
workflow_dispatch:

permissions:
contents: write
pull-requests: write
issues: write

concurrency:
group: upstream-apm-sync
cancel-in-progress: false

env:
UPSTREAM_REPO: https://github.com/microsoft/apm.git
UPSTREAM_BRANCH: main
SYNC_BRANCH: automation/upstream-microsoft-apm-main

jobs:
sync:
name: Sync microsoft/apm main
runs-on: ubuntu-24.04
steps:
- name: Check out main
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0

- name: Configure git identity
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Fetch upstream
run: |
git remote add upstream "$UPSTREAM_REPO" 2>/dev/null || \
git remote set-url upstream "$UPSTREAM_REPO"
git fetch upstream "$UPSTREAM_BRANCH" --prune
git fetch origin main --prune

- name: Merge upstream into sync branch
id: merge
shell: bash
run: |
upstream_ref="upstream/${UPSTREAM_BRANCH}"
upstream_sha="$(git rev-parse "$upstream_ref")"
origin_sha="$(git rev-parse origin/main)"
echo "upstream_sha=$upstream_sha" >> "$GITHUB_OUTPUT"
echo "origin_sha=$origin_sha" >> "$GITHUB_OUTPUT"

if git merge-base --is-ancestor "$upstream_ref" origin/main; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "origin/main already contains ${upstream_ref} (${upstream_sha})."
exit 0
fi

git switch --force-create "$SYNC_BRANCH" origin/main
if ! git merge --no-ff --no-edit "$upstream_ref"; then
git status --short
echo "::error::Automatic upstream merge conflicted. Resolve manually by merging ${upstream_ref} into main."
exit 1
fi
git push --force-with-lease origin "$SYNC_BRANCH"
echo "changed=true" >> "$GITHUB_OUTPUT"

- name: Create or update sync PR
if: steps.merge.outputs.changed == 'true'
id: pr
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
body="$RUNNER_TEMP/upstream-sync-pr.md"
cat > "$body" <<EOF
## TL;DR

This PR merges \`microsoft/apm@${{ steps.merge.outputs.upstream_sha }}\` into \`githubnext/apm@main\`.

## Required follow-up before Go migration completion

- Review the upstream Python diff for new CLI/runtime behavior.
- Add or update real Go behavior tests for every changed Python source/test contract.
- Advance \`tests/parity/upstream_contract_coverage.yml\` with a reviewed range from the previous upstream SHA to \`${{ steps.merge.outputs.upstream_sha }}\`.
- Run the enforcing migration gate before declaring issue #78 complete.

This PR is created by \`.github/workflows/upstream-apm-sync.yml\`.
EOF

pr_number="$(gh pr list \
--head "$SYNC_BRANCH" \
--base main \
--state open \
--json number \
--jq '.[0].number')"

if [ -z "$pr_number" ]; then
pr_url="$(gh pr create \
--base main \
--head "$SYNC_BRANCH" \
--title "chore(upstream): merge microsoft/apm main" \
--body-file "$body")"
pr_number="${pr_url##*/}"
else
gh pr edit "$pr_number" --body-file "$body"
fi

echo "number=$pr_number" >> "$GITHUB_OUTPUT"
gh pr view "$pr_number" --json url --jq .url

- name: Request merge-commit auto-merge
if: steps.merge.outputs.changed == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
if gh pr merge "${{ steps.pr.outputs.number }}" --auto --merge --delete-branch; then
echo "Auto-merge requested for upstream sync PR #${{ steps.pr.outputs.number }}."
else
echo "::warning::Could not enable auto-merge. PR #${{ steps.pr.outputs.number }} is ready for maintainer review/merge."
fi

- name: Summarize
if: always()
run: |
{
echo "## Upstream APM Sync"
echo
echo "- Upstream: \`${UPSTREAM_REPO}\`"
echo "- Branch: \`${UPSTREAM_BRANCH}\`"
echo "- Sync branch: \`${SYNC_BRANCH}\`"
echo "- Changed: \`${{ steps.merge.outputs.changed || 'unknown' }}\`"
echo "- Upstream SHA: \`${{ steps.merge.outputs.upstream_sha || 'unknown' }}\`"
echo "- Origin SHA: \`${{ steps.merge.outputs.origin_sha || 'unknown' }}\`"
} >> "$GITHUB_STEP_SUMMARY"
Loading
Loading