potiuk commented on code in PR #74311: URL: https://github.com/apache/airflow/pull/74311#discussion_r4307908256
########## .github/workflows/dependency-upgrade-report.yml: ########## @@ -0,0 +1,167 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +--- +name: Dependency upgrade report +# Both the job gate and the API validation reject PRs and foreign repositories. Only +# completed main schedule/dispatch runs are executed, with a read-only token and no secrets. +"on": # zizmor: ignore[dangerous-triggers] + workflow_run: + workflows: ["Tests (AMD)"] + types: [completed] + branches: [main] +permissions: + contents: read + actions: read +concurrency: + group: dependency-report-${{ github.event.workflow_run.id }} + cancel-in-progress: false +jobs: + plan: + runs-on: ubuntu-slim + timeout-minutes: 5 + if: >- + github.event.workflow_run.head_repository.full_name == github.repository && + (github.event.workflow_run.event == 'schedule' || + github.event.workflow_run.event == 'workflow_dispatch') + outputs: + matrix: ${{ steps.plan.outputs.matrix }} + has-reports: ${{ steps.plan.outputs.has-reports }} + source-sha: ${{ steps.plan.outputs.source-sha }} + source-run-id: ${{ steps.plan.outputs.source-run-id }} + steps: + - name: "Select inputs from the original canary run" + id: plan + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + SOURCE_RUN_ID: ${{ github.event.workflow_run.id }} + with: + script: | + const rawId = process.env.SOURCE_RUN_ID; + if (!/^[1-9][0-9]*$/.test(rawId)) throw new Error('Invalid source run ID'); + const run_id = Number(rawId); + if (!Number.isSafeInteger(run_id)) throw new Error('Invalid source run ID'); + const repo = context.repo; + const { data: run } = await github.rest.actions.getWorkflowRun({ ...repo, run_id }); + if (run.head_repository.full_name !== `${repo.owner}/${repo.repo}` || + run.head_branch !== 'main' || run.path !== '.github/workflows/ci-amd.yml' || + !['schedule', 'workflow_dispatch'].includes(run.event) || run.status !== 'completed') { + throw new Error('Only completed main AMD canary runs are supported'); + } + const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, + { ...repo, run_id, per_page: 100 }); + const include = []; + const inputName = new RegExp('^dependency-report-inputs-(3\\.[0-9]+)-' + + '(constraints(?:-source-providers|-no-providers)?)-(true|false)$'); + for (const input of artifacts) { + const match = inputName.exec(input.name); + if (!match) continue; + const [, python, mode, useUv] = match; + const image = artifacts.find(a => a.name === `ci-image-save-v3-linux_amd64-${python}-main`); + if (input.expired || !image || image.expired) { + throw new Error(`Original inputs or CI image unavailable for ${python}:${mode}`); + } Review Comment: **One missing artifact stops every report.** In practice that looks likely: today's 02:00 `main` canary (run 39088484466) built CI images for 3.11 to 3.14, but 15 hours later only `ci-image-save-v3-linux_amd64-3.12-main` is still listed on that run. The stash appears to keep only the newest artifact per key and branch, so by the time a report runs, the other versions' images may already be gone. Skipping the unavailable entries (and listing them in the summary) keeps the reports that can still be built; `test_plan_does_not_substitute_newer_artifacts` would then assert the entry is skipped. Longer term, saving the image under a dedicated artifact name that later stashes don't replace would make the report independent of that. ```suggestion if (input.expired || !image || image.expired) { core.warning(`Skipping ${python}:${mode}: original inputs or CI image unavailable`); continue; } ``` ########## .github/workflows/dependency-upgrade-report.yml: ########## @@ -0,0 +1,167 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +--- +name: Dependency upgrade report +# Both the job gate and the API validation reject PRs and foreign repositories. Only +# completed main schedule/dispatch runs are executed, with a read-only token and no secrets. +"on": # zizmor: ignore[dangerous-triggers] + workflow_run: + workflows: ["Tests (AMD)"] + types: [completed] + branches: [main] +permissions: + contents: read + actions: read +concurrency: + group: dependency-report-${{ github.event.workflow_run.id }} + cancel-in-progress: false Review Comment: Re-running the source canary (e.g. one flaky job) fires `workflow_run` again with the same run id, and with `cancel-in-progress: false` that queues a second full report from the same inputs. ########## .github/workflows/dependency-upgrade-report.yml: ########## @@ -0,0 +1,167 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +--- +name: Dependency upgrade report +# Both the job gate and the API validation reject PRs and foreign repositories. Only +# completed main schedule/dispatch runs are executed, with a read-only token and no secrets. +"on": # zizmor: ignore[dangerous-triggers] + workflow_run: + workflows: ["Tests (AMD)"] + types: [completed] + branches: [main] +permissions: + contents: read + actions: read +concurrency: + group: dependency-report-${{ github.event.workflow_run.id }} + cancel-in-progress: false +jobs: + plan: + runs-on: ubuntu-slim + timeout-minutes: 5 + if: >- + github.event.workflow_run.head_repository.full_name == github.repository && + (github.event.workflow_run.event == 'schedule' || + github.event.workflow_run.event == 'workflow_dispatch') + outputs: + matrix: ${{ steps.plan.outputs.matrix }} + has-reports: ${{ steps.plan.outputs.has-reports }} + source-sha: ${{ steps.plan.outputs.source-sha }} + source-run-id: ${{ steps.plan.outputs.source-run-id }} + steps: + - name: "Select inputs from the original canary run" + id: plan + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + SOURCE_RUN_ID: ${{ github.event.workflow_run.id }} + with: + script: | + const rawId = process.env.SOURCE_RUN_ID; + if (!/^[1-9][0-9]*$/.test(rawId)) throw new Error('Invalid source run ID'); + const run_id = Number(rawId); + if (!Number.isSafeInteger(run_id)) throw new Error('Invalid source run ID'); + const repo = context.repo; + const { data: run } = await github.rest.actions.getWorkflowRun({ ...repo, run_id }); + if (run.head_repository.full_name !== `${repo.owner}/${repo.repo}` || + run.head_branch !== 'main' || run.path !== '.github/workflows/ci-amd.yml' || + !['schedule', 'workflow_dispatch'].includes(run.event) || run.status !== 'completed') { + throw new Error('Only completed main AMD canary runs are supported'); + } + const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, + { ...repo, run_id, per_page: 100 }); + const include = []; + const inputName = new RegExp('^dependency-report-inputs-(3\\.[0-9]+)-' + + '(constraints(?:-source-providers|-no-providers)?)-(true|false)$'); + for (const input of artifacts) { + const match = inputName.exec(input.name); + if (!match) continue; + const [, python, mode, useUv] = match; + const image = artifacts.find(a => a.name === `ci-image-save-v3-linux_amd64-${python}-main`); + if (input.expired || !image || image.expired) { + throw new Error(`Original inputs or CI image unavailable for ${python}:${mode}`); + } + include.push({ python, mode, 'use-uv': useUv, + 'inputs-id': input.id, 'image-id': image.id }); + } + core.setOutput('matrix', JSON.stringify({ include })); + core.setOutput('has-reports', include.length > 0 ? 'true' : 'false'); + core.setOutput('source-sha', run.head_sha); + core.setOutput('source-run-id', rawId); + if (!include.length) { + if (Number(process.env.GITHUB_RUN_ATTEMPT) > 1) { Review Comment: A re-run of a report whose canary saved no inputs fails hard here, while attempt 1 of the same run only posts a notice. That wrongly suggests inputs once existed and expired. ########## .github/workflows/dependency-upgrade-report.yml: ########## @@ -0,0 +1,167 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +--- +name: Dependency upgrade report +# Both the job gate and the API validation reject PRs and foreign repositories. Only +# completed main schedule/dispatch runs are executed, with a read-only token and no secrets. +"on": # zizmor: ignore[dangerous-triggers] + workflow_run: + workflows: ["Tests (AMD)"] + types: [completed] + branches: [main] +permissions: + contents: read + actions: read +concurrency: + group: dependency-report-${{ github.event.workflow_run.id }} + cancel-in-progress: false +jobs: + plan: + runs-on: ubuntu-slim + timeout-minutes: 5 + if: >- + github.event.workflow_run.head_repository.full_name == github.repository && + (github.event.workflow_run.event == 'schedule' || + github.event.workflow_run.event == 'workflow_dispatch') + outputs: + matrix: ${{ steps.plan.outputs.matrix }} + has-reports: ${{ steps.plan.outputs.has-reports }} + source-sha: ${{ steps.plan.outputs.source-sha }} + source-run-id: ${{ steps.plan.outputs.source-run-id }} + steps: + - name: "Select inputs from the original canary run" + id: plan + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + SOURCE_RUN_ID: ${{ github.event.workflow_run.id }} + with: + script: | + const rawId = process.env.SOURCE_RUN_ID; + if (!/^[1-9][0-9]*$/.test(rawId)) throw new Error('Invalid source run ID'); + const run_id = Number(rawId); + if (!Number.isSafeInteger(run_id)) throw new Error('Invalid source run ID'); + const repo = context.repo; + const { data: run } = await github.rest.actions.getWorkflowRun({ ...repo, run_id }); + if (run.head_repository.full_name !== `${repo.owner}/${repo.repo}` || + run.head_branch !== 'main' || run.path !== '.github/workflows/ci-amd.yml' || + !['schedule', 'workflow_dispatch'].includes(run.event) || run.status !== 'completed') { + throw new Error('Only completed main AMD canary runs are supported'); + } + const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, + { ...repo, run_id, per_page: 100 }); + const include = []; + const inputName = new RegExp('^dependency-report-inputs-(3\\.[0-9]+)-' + + '(constraints(?:-source-providers|-no-providers)?)-(true|false)$'); + for (const input of artifacts) { + const match = inputName.exec(input.name); + if (!match) continue; + const [, python, mode, useUv] = match; + const image = artifacts.find(a => a.name === `ci-image-save-v3-linux_amd64-${python}-main`); + if (input.expired || !image || image.expired) { + throw new Error(`Original inputs or CI image unavailable for ${python}:${mode}`); + } + include.push({ python, mode, 'use-uv': useUv, + 'inputs-id': input.id, 'image-id': image.id }); + } + core.setOutput('matrix', JSON.stringify({ include })); + core.setOutput('has-reports', include.length > 0 ? 'true' : 'false'); + core.setOutput('source-sha', run.head_sha); + core.setOutput('source-run-id', rawId); + if (!include.length) { + if (Number(process.env.GITHUB_RUN_ATTEMPT) > 1) { + throw new Error('Saved report inputs are missing or expired'); + } + core.notice('No saved report inputs: this run did not request independent diagnostics'); + } + await core.summary.addHeading('Dependency upgrade diagnostics') + .addLink('Source canary run', run.html_url) + .addRaw(`\n\nSource revision: ${run.head_sha}\n\nReports: ${include.length}\n`) + .write(); + + explain: + needs: plan + if: needs.plan.outputs.has-reports == 'true' + name: "Explain ${{ matrix.python }}:${{ matrix.mode }}" + runs-on: ubuntu-22.04 + timeout-minutes: 90 + strategy: + fail-fast: false + max-parallel: 3 + matrix: ${{ fromJSON(needs.plan.outputs.matrix) }} + env: + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_USERNAME: ${{ github.actor }} + VERBOSE: "false" + USE_UV: ${{ matrix.use-uv }} + steps: + - name: "Checkout the tested revision" + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.plan.outputs.source-sha }} + persist-credentials: false + - name: "Install Breeze" + uses: ./.github/actions/breeze + - name: "Make /mnt writeable" + run: ./scripts/ci/make_mnt_writeable.sh Review Comment: This job loads a multi-GB image and runs `uv sync` with `--refresh` resolutions, but without `free_up_disk_space.sh` / `move_docker_to_mnt.sh` like the other image jobs, so Docker stays on `/` and the job can run out of disk partway. ########## .github/workflows/dependency-upgrade-report.yml: ########## @@ -0,0 +1,167 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +--- +name: Dependency upgrade report +# Both the job gate and the API validation reject PRs and foreign repositories. Only +# completed main schedule/dispatch runs are executed, with a read-only token and no secrets. +"on": # zizmor: ignore[dangerous-triggers] + workflow_run: + workflows: ["Tests (AMD)"] + types: [completed] + branches: [main] +permissions: + contents: read + actions: read +concurrency: + group: dependency-report-${{ github.event.workflow_run.id }} + cancel-in-progress: false +jobs: + plan: + runs-on: ubuntu-slim + timeout-minutes: 5 + if: >- + github.event.workflow_run.head_repository.full_name == github.repository && + (github.event.workflow_run.event == 'schedule' || + github.event.workflow_run.event == 'workflow_dispatch') + outputs: + matrix: ${{ steps.plan.outputs.matrix }} + has-reports: ${{ steps.plan.outputs.has-reports }} + source-sha: ${{ steps.plan.outputs.source-sha }} + source-run-id: ${{ steps.plan.outputs.source-run-id }} + steps: + - name: "Select inputs from the original canary run" + id: plan + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + SOURCE_RUN_ID: ${{ github.event.workflow_run.id }} + with: + script: | + const rawId = process.env.SOURCE_RUN_ID; + if (!/^[1-9][0-9]*$/.test(rawId)) throw new Error('Invalid source run ID'); + const run_id = Number(rawId); + if (!Number.isSafeInteger(run_id)) throw new Error('Invalid source run ID'); + const repo = context.repo; + const { data: run } = await github.rest.actions.getWorkflowRun({ ...repo, run_id }); + if (run.head_repository.full_name !== `${repo.owner}/${repo.repo}` || + run.head_branch !== 'main' || run.path !== '.github/workflows/ci-amd.yml' || + !['schedule', 'workflow_dispatch'].includes(run.event) || run.status !== 'completed') { + throw new Error('Only completed main AMD canary runs are supported'); + } + const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, + { ...repo, run_id, per_page: 100 }); + const include = []; + const inputName = new RegExp('^dependency-report-inputs-(3\\.[0-9]+)-' + + '(constraints(?:-source-providers|-no-providers)?)-(true|false)$'); + for (const input of artifacts) { + const match = inputName.exec(input.name); + if (!match) continue; + const [, python, mode, useUv] = match; + const image = artifacts.find(a => a.name === `ci-image-save-v3-linux_amd64-${python}-main`); Review Comment: This name comes from the stash action's internal naming scheme. It matches what the action produces today (I checked the real artifact), but nothing in this repo guarantees it, and only a mocked test uses it. ########## dev/breeze/doc/ci/05_workflows.md: ########## @@ -368,6 +368,35 @@ unprivileged, which constrains what can go on it: When adding a job, reach for `ubuntu-slim` if it only shuffles metadata around, and `ubuntu-22.04` otherwise. +### Dependency upgrade diagnostics + +Scheduled and manually dispatched AMD canaries on `main` keep the dependency freshness +summary in `finalize-tests.yml`, but run it with `--no-explain-why`. Tests, constraint +generation and validation, and image publication still run in the canary. Release-branch +and other run types retain their existing explanation behavior. + +After the canary completes, `dependency-upgrade-report.yml` runs the detailed explanations +independently, including when tests failed but the report inputs were saved. It checks out +the original source SHA and downloads the original image and saved constraints by artifact +ID from that run. It never substitutes the latest branch image or constraints. PyPI release +metadata is fetched when the report executes, so the available upgrade targets can change +between the summary and the explanations. + +The workflow has read-only repository permissions and runs only on `workflow_run`, which +cannot write to the default branch's GitHub Actions cache. Reports use the saved image artifact. Review Comment: As far as I know, `workflow_run` runs in the default branch's context and can write to its cache. What protects the cache here is that no step saves one, which is what this should say, so nobody later adds a cache step thinking it is safe. ########## dev/breeze/tests/test_dependency_upgrade_report.py: ########## @@ -0,0 +1,168 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +from __future__ import annotations + +import json +import subprocess + +import pytest +import yaml + +from airflow_breeze.utils.path_utils import AIRFLOW_ROOT_PATH + +NODE_HARNESS = """ +import { readFileSync } from 'node:fs'; +const input = JSON.parse(readFileSync(0, 'utf8')); +process.env.SOURCE_RUN_ID = input.id; +process.env.GITHUB_RUN_ATTEMPT = String(input.attempt); +const outputs = {}; +const requests = []; +const summary = { + addHeading() { return this; }, addLink() { return this; }, + addRaw() { return this; }, async write() {} +}; +const github = { + rest: { actions: { + async getWorkflowRun(args) { requests.push(args); return { data: input.run }; }, + listWorkflowRunArtifacts() {} + } }, + async paginate(method, args) { requests.push(args); return input.artifacts; } +}; +const core = { setOutput(key, value) { outputs[key] = value; }, summary, notice() {} }; +const context = { repo: { owner: 'apache', repo: 'airflow' } }; +const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; +try { + await new AsyncFunction('github', 'context', 'core', input.script)(github, context, core); + console.log(JSON.stringify({ outputs, requests })); +} catch (error) { + console.log(JSON.stringify({ error: error.message, requests })); +} +""" + + [email protected] +def source_run(): + return { + "head_repository": {"full_name": "apache/airflow"}, + "head_branch": "main", + "path": ".github/workflows/ci-amd.yml", + "event": "schedule", + "status": "completed", + "head_sha": "a" * 40, + "html_url": "https://github.com/apache/airflow/actions/runs/123", + } + + [email protected] +def source_artifacts(): + return [ + { + "id": 1, + "name": "dependency-report-inputs-3.14-constraints-source-providers-true", + "expired": False, + }, + {"id": 2, "name": "ci-image-save-v3-linux_amd64-3.14-main", "expired": False}, + ] + + +def run_plan(source_run, source_artifacts, *, attempt=1, source_run_id="123"): + workflow = yaml.safe_load( + (AIRFLOW_ROOT_PATH / ".github/workflows/dependency-upgrade-report.yml").read_text() + ) + script = workflow["jobs"]["plan"]["steps"][0]["with"]["script"] + result = subprocess.run( + ["node", "--input-type=module", "--eval", NODE_HARNESS], Review Comment: These tests need `node` on PATH. A `pytest.mark.skipif(shutil.which("node") is None, …)` would keep `uv run --project dev/breeze pytest` working for people without it. ########## .github/workflows/finalize-tests.yml: ########## @@ -149,15 +149,54 @@ jobs: python: ${{ matrix.python-version }} use-uv: ${{ inputs.use-uv }} make-mnt-writeable-and-cleanup: true + - name: "Save constraints for the independent dependency report" + shell: bash + if: >- + github.ref == 'refs/heads/main' && + (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CONSTRAINTS_BRANCH: ${{ inputs.constraints-branch }} + PYTHON_VERSION: ${{ matrix.python-version }} + CONSTRAINTS_MODE: ${{ matrix.constraints-mode }} + run: | + mkdir -p files/dependency-report + REF_API="repos/apache/airflow/git/ref/heads/${CONSTRAINTS_BRANCH}" + CONSTRAINTS_SHA=$(gh api "${REF_API}" --jq '.object.sha') + FILE_API="repos/apache/airflow/contents/${CONSTRAINTS_MODE}-${PYTHON_VERSION}.txt" + gh api "${FILE_API}?ref=${CONSTRAINTS_SHA}" \ + --jq '.content' | base64 --decode > files/dependency-report/constraints.txt Review Comment: **The canary now depends on raw API calls.** Two `gh api … | base64 --decode` calls under `pipefail` per Deps job mean a transient API error or rate limit turns the canary red. The repo is hard-coded to `apache/airflow` (ignoring `CONSTRAINTS_GITHUB_REPOSITORY`), and this duplicates breeze's `download_constraints_file`; a `--save-constraints-to` option on the breeze command would keep one code path. Also, these constraints come from the branch tip at summary time, while the report checks out the canary's `head_sha`. If `update-constraints-on-push` moved the branch in between, version-bound changes show up as false conflicts or "stale constraints". -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
