From 70d18cd6d4d58e73223ae352007ed0a7685723c7 Mon Sep 17 00:00:00 2001 From: Yosh Date: Sun, 8 Nov 2026 02:46:04 +0100 Subject: [PATCH 01/14] Apply structured labels to repository automation Signed-off-by: Yosh Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/bug.md | 2 +- .github/dependabot.yml | 4 +- .github/release.yml | 4 +- .github/workflows/Coverage.yml | 2 +- .github/workflows/DailyArm64.yml | 10 +- .github/workflows/DailyBenchmarks.yml | 2 +- .github/workflows/Fuzzing.yml | 2 +- .github/workflows/IssueLabelChecker.yml | 21 +- .github/workflows/PRLabelChecker.yml | 17 +- .github/workflows/ReleaseBlockerCheck.yml | 2 +- .../workflows/ReleaseBlockerLabelCleanUp.yml | 22 +- .github/workflows/RustNightly.yml | 2 +- .github/workflows/ValidatePullRequest.yml | 6 +- .github/workflows/WeeklyMshv618.yml | 7 +- .github/workflows/ready-for-review-label.yml | 2 +- .github/workflows/ready-for-review-manage.yml | 8 +- Justfile | 8 +- dev/check-pr-labels.jq | 12 + dev/check-release-blockers.sh | 25 +- dev/ensure-issue-status.sh | 14 + dev/label-aliases.json | 15 ++ dev/label-utils.sh | 18 ++ dev/notify-ci-failure.sh | 80 +++--- dev/test-label-migration.py | 247 ++++++++++++++++++ docs/how-to-make-releases.md | 8 +- docs/snapshot-versioning.md | 18 +- 26 files changed, 437 insertions(+), 121 deletions(-) create mode 100644 dev/check-pr-labels.jq create mode 100644 dev/ensure-issue-status.sh create mode 100644 dev/label-aliases.json create mode 100644 dev/label-utils.sh create mode 100644 dev/test-label-migration.py diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index d933d87883..3b6fcac24d 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -2,7 +2,7 @@ name: Bug Report about: Report a bug in Hyperlight title: '' -labels: bug +labels: 'C-bug, kind/bugfix' assignees: '' --- diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 30d347fcb2..631b1e77ba 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,6 +9,7 @@ updates: time: "03:00" labels: - "kind/dependencies" + - "C-deps" - package-ecosystem: "cargo" directory: "/" cooldown: @@ -18,6 +19,7 @@ updates: time: "03:00" labels: - "kind/dependencies" + - "C-deps" open-pull-requests-limit: 20 groups: windows: @@ -41,7 +43,7 @@ updates: time: "03:00" labels: - "kind/dependencies" - - "area/guest" + - "C-deps" open-pull-requests-limit: 5 groups: windows: diff --git a/.github/release.yml b/.github/release.yml index 0110edad59..5ab81660ff 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -8,6 +8,8 @@ changelog: exclude: labels: - kind/dependencies + - C-deps - title: Full Changelog (dependencies) labels: - - kind/dependencies \ No newline at end of file + - kind/dependencies + - C-deps \ No newline at end of file diff --git a/.github/workflows/Coverage.yml b/.github/workflows/Coverage.yml index 11efb1cb00..9fb8fa73bb 100644 --- a/.github/workflows/Coverage.yml +++ b/.github/workflows/Coverage.yml @@ -94,6 +94,6 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Notify Coverage Failure - run: ./dev/notify-ci-failure.sh --title="Weekly Coverage Failure - ${{ github.run_number }}" --labels="area/ci-periodics,area/testing,lifecycle/needs-review" + run: ./dev/notify-ci-failure.sh --title="Weekly Coverage Failure - ${{ github.run_number }}" --labels="A-ci-periodics,A-testing,S-needs-triage" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/DailyArm64.yml b/.github/workflows/DailyArm64.yml index 952c6416eb..c8411c6701 100644 --- a/.github/workflows/DailyArm64.yml +++ b/.github/workflows/DailyArm64.yml @@ -102,11 +102,9 @@ jobs: timeout_minutes: 35 # 5 minutes fuzzing plus setup and build on slower arm64 runners # File (or update) a release-blocking GitHub issue if any job fails. The first - # label (area/ci-periodics-aarch64) is the de-duplication key used by - # notify-ci-failure.sh, so this workflow maintains its own issue lineage and is - # guaranteed to carry release-blocker, instead of commenting on another periodic - # job's issue. NOTE: that label must exist in the repo (Issues -> Labels) or the - # `gh issue create` call will fail. + # label (A-ci-periodics-aarch64) keeps these failures on a dedicated issue. + # The notifier accepts an existing rollout alias and ensures the issue + # carries the release blocker label. notify-failure: runs-on: ubuntu-latest needs: [build-guests, miri, build-test, run-examples, fuzzing] @@ -121,6 +119,6 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Notify Daily aarch64 Failure - run: ./dev/notify-ci-failure.sh --title="Daily aarch64 Failure - ${{ github.run_number }}" --labels="area/ci-periodics-aarch64,area/ci-periodics,area/testing,lifecycle/needs-review,release-blocker" + run: ./dev/notify-ci-failure.sh --title="Daily aarch64 Failure - ${{ github.run_number }}" --labels="A-ci-periodics-aarch64,A-ci-periodics,A-testing,S-needs-triage,P-release-blocker" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/DailyBenchmarks.yml b/.github/workflows/DailyBenchmarks.yml index 9ece6a5ad8..c3a56f22df 100644 --- a/.github/workflows/DailyBenchmarks.yml +++ b/.github/workflows/DailyBenchmarks.yml @@ -66,6 +66,6 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Notify Benchmark Failure - run: ./dev/notify-ci-failure.sh --title="Benchmark Failure - ${{ github.run_number }}" --labels="area/benchmarks,area/testing,lifecycle/needs-review,release-blocker" + run: ./dev/notify-ci-failure.sh --title="Benchmark Failure - ${{ github.run_number }}" --labels="A-perf,A-testing,S-needs-triage,P-release-blocker" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/Fuzzing.yml b/.github/workflows/Fuzzing.yml index 522211b07d..c203bfcc3d 100644 --- a/.github/workflows/Fuzzing.yml +++ b/.github/workflows/Fuzzing.yml @@ -44,6 +44,6 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Notify Fuzzing Failure - run: ./dev/notify-ci-failure.sh --labels="area/fuzzing,area/testing,lifecycle/needs-review,release-blocker" + run: ./dev/notify-ci-failure.sh --labels="A-fuzzing,A-testing,S-needs-triage,P-release-blocker" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/IssueLabelChecker.yml b/.github/workflows/IssueLabelChecker.yml index 6b117eb4f9..40ddadb0f6 100644 --- a/.github/workflows/IssueLabelChecker.yml +++ b/.github/workflows/IssueLabelChecker.yml @@ -14,22 +14,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Check and Add label run: | - # The cryptic head -c -1 is because otherwise gh always terminates output with a newline - readarray -d $'\0' lifecycles < <(gh issue view ${{ github.event.issue.number }} --json labels -q '[.labels[] | .name | select(startswith("lifecycle/"))] | join("\u0000")' | head -c -1) - if [[ ${#lifecycles[@]} -ne 1 ]]; then - if [[ ${#lifecycles[@]} -ge 1 ]]; then - echo 'Too many lifecycle labels; replacing all with `lifecycle/needs-review`' - fi - commands=() - for label in "${lifecycles[@]}"; do - if [[ "$label" != "lifecycle/needs-review" ]]; then - echo "Removing label ${label}" - commands+=("--remove-label" "${label}") - fi - done - echo 'Adding `lifecycle/needs review`' - commands+=("--add-label" "lifecycle/needs-review") - gh issue edit ${{ github.event.issue.number }} "${commands[@]}" - fi + bash dev/ensure-issue-status.sh "${{ github.event.issue.number }}" env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} \ No newline at end of file diff --git a/.github/workflows/PRLabelChecker.yml b/.github/workflows/PRLabelChecker.yml index 94e0db16c5..8ed76079bc 100644 --- a/.github/workflows/PRLabelChecker.yml +++ b/.github/workflows/PRLabelChecker.yml @@ -12,18 +12,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Ensure exactly one "kind/*" label is applied + - name: Test label migration offline + run: python3 dev/test-label-migration.py + - name: Ensure exactly one category is applied run: | - # Count the number of "kind/*" labels directly from the PR labels + set -euo pipefail PR_NUMBER=${{ github.event.pull_request.number }} - KIND_LABEL_COUNT=$(gh pr view "$PR_NUMBER" --json labels -q '.labels.[].name' | grep -c '^kind/') - - if [[ "$KIND_LABEL_COUNT" -eq 1 ]]; then - echo "✅ Exactly one 'kind/*' label is applied." - exit 0 - else - echo "❌ PR must have exactly one 'kind/*' label, but found $KIND_LABEL_COUNT." - exit 1 - fi + gh pr view "$PR_NUMBER" --json labels | + jq -e --slurpfile aliases dev/label-aliases.json -f dev/check-pr-labels.jq env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ReleaseBlockerCheck.yml b/.github/workflows/ReleaseBlockerCheck.yml index f034181211..e4a3b488e3 100644 --- a/.github/workflows/ReleaseBlockerCheck.yml +++ b/.github/workflows/ReleaseBlockerCheck.yml @@ -34,7 +34,7 @@ jobs: if [[ "${{ github.ref }}" == *"refs/heads/release/"* ]]; then echo "Release branch detected - checking for release blockers..." if ! ./dev/check-release-blockers.sh "$REPO"; then - echo "::error::Release blocked by open issues with 'release-blocker' label" + echo "::error::Release blocker check failed (P-release-blocker or release-blocker)" exit 1 fi else diff --git a/.github/workflows/ReleaseBlockerLabelCleanUp.yml b/.github/workflows/ReleaseBlockerLabelCleanUp.yml index 537272e123..7a9b3a6292 100644 --- a/.github/workflows/ReleaseBlockerLabelCleanUp.yml +++ b/.github/workflows/ReleaseBlockerLabelCleanUp.yml @@ -14,20 +14,16 @@ jobs: steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Remove release-blocker label from closed issue + - name: Remove release blocker labels from closed issue run: | + set -euo pipefail ISSUE_NUMBER=${{ github.event.issue.number }} - echo "Checking if issue #$ISSUE_NUMBER has release-blocker label..." - - # Check if the issue has the release-blocker label - HAS_LABEL=$(gh issue view "$ISSUE_NUMBER" --json labels -q '.labels[] | select(.name == "release-blocker") | .name') - - if [ -n "$HAS_LABEL" ]; then - echo "✅ Issue #$ISSUE_NUMBER has release-blocker label, removing it..." - gh issue edit "$ISSUE_NUMBER" --remove-label "release-blocker" - echo "✅ Successfully removed release-blocker label from issue #$ISSUE_NUMBER" - else - echo "ℹ️ Issue #$ISSUE_NUMBER does not have release-blocker label, no action needed" - fi + LABELS="$(gh issue view "$ISSUE_NUMBER" --json labels)" + LABEL_NAMES="$(jq -e '[.labels[].name]' <<<"$LABELS")" + for label in P-release-blocker release-blocker; do + if jq -e --arg label "$label" 'index($label) != null' <<<"$LABEL_NAMES" >/dev/null; then + gh issue edit "$ISSUE_NUMBER" --remove-label "$label" + fi + done env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/RustNightly.yml b/.github/workflows/RustNightly.yml index 61aabddc88..62ad5f728d 100644 --- a/.github/workflows/RustNightly.yml +++ b/.github/workflows/RustNightly.yml @@ -124,6 +124,6 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Notify Nightly Failure - run: ./dev/notify-ci-failure.sh --title="Nightly musl Failure - ${{ github.run_number }}" --labels="area/ci-periodics-nightly,area/ci-periodics,area/testing,lifecycle/needs-review,release-blocker" + run: ./dev/notify-ci-failure.sh --title="Nightly musl Failure - ${{ github.run_number }}" --labels="A-ci-periodics-nightly,A-ci-periodics,A-testing,S-needs-triage,P-release-blocker" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ValidatePullRequest.yml b/.github/workflows/ValidatePullRequest.yml index 568497f8a7..7aff52c993 100644 --- a/.github/workflows/ValidatePullRequest.yml +++ b/.github/workflows/ValidatePullRequest.yml @@ -98,7 +98,7 @@ jobs: with: docs_only: ${{ needs.docs-pr.outputs.docs-only }} - # Pick the goldens mode. The `regen-goldens` label means regenerate. No label means pull. + # S-needs-goldens and its rollout alias select regenerate mode. # Also validate the label against the goldens state so a stale or missing label fails fast. check-golden-label: runs-on: ubuntu-latest @@ -135,13 +135,13 @@ jobs: run: | labels="$(gh pr view ${{ github.event.pull_request.number }} \ --repo ${{ github.repository }} --json labels -q '.labels[].name')" - if grep -qx regen-goldens <<<"$labels"; then + if grep -Eqx 'S-needs-goldens|regen-goldens' <<<"$labels"; then echo "should_regen_goldens=true" >> "$GITHUB_OUTPUT" else echo "should_regen_goldens=false" >> "$GITHUB_OUTPUT" fi - - name: Validate regen-goldens label + - name: Validate S-needs-goldens label if: github.event_name == 'pull_request' env: BASE_REF: ${{ github.event.pull_request.base.ref }} diff --git a/.github/workflows/WeeklyMshv618.yml b/.github/workflows/WeeklyMshv618.yml index 13ece032d9..d0ac26f3a8 100644 --- a/.github/workflows/WeeklyMshv618.yml +++ b/.github/workflows/WeeklyMshv618.yml @@ -63,9 +63,8 @@ jobs: arch: X64 config: ${{ matrix.config }} - # The first label is the de-duplication key used by notify-ci-failure.sh, so - # these failures share one release-blocking issue. That label must exist in - # the repo or `gh issue create` fails. + # The first label keeps these failures on a dedicated release-blocking issue. + # The notifier accepts an existing rollout alias. notify-failure: runs-on: ubuntu-latest needs: [build-guests, build-test, run-examples] @@ -79,6 +78,6 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Notify Weekly MSHV 6.18 Failure - run: ./dev/notify-ci-failure.sh --title="Weekly MSHV 6.18 Failure - ${{ github.run_number }}" --labels="area/ci-periodics-mshv-6.18,area/ci-periodics,area/testing,lifecycle/needs-review,release-blocker" + run: ./dev/notify-ci-failure.sh --title="Weekly MSHV 6.18 Failure - ${{ github.run_number }}" --labels="A-ci-periodics-mshv-6.18,A-ci-periodics,A-testing,S-needs-triage,P-release-blocker" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ready-for-review-label.yml b/.github/workflows/ready-for-review-label.yml index c9e23128ab..bb3b21d392 100644 --- a/.github/workflows/ready-for-review-label.yml +++ b/.github/workflows/ready-for-review-label.yml @@ -1,6 +1,6 @@ name: Ready-for-review (collect) -# Stage 1 of a two-stage pipeline that removes the "ready-for-review" label from +# Stage 1 of a two-stage pipeline that removes S-needs-review or ready-for-review from # a pull request once it is no longer awaiting review. # # Pull-request and review events originating from FORKED repositories only ever diff --git a/.github/workflows/ready-for-review-manage.yml b/.github/workflows/ready-for-review-manage.yml index 7eff797694..15c63b85f2 100644 --- a/.github/workflows/ready-for-review-manage.yml +++ b/.github/workflows/ready-for-review-manage.yml @@ -1,6 +1,6 @@ name: Ready-for-review (manage) -# Stage 2 of the two-stage "ready-for-review" label pipeline. It is triggered +# Stage 2 of the S-needs-review label pipeline, including its rollout alias. It is triggered # when "Ready-for-review (collect)" completes and runs in the context of the # base repository's default branch, so it receives a read-write GITHUB_TOKEN # even when the originating event came from a fork. This is what lets us remove @@ -13,7 +13,7 @@ name: Ready-for-review (manage) # * strictly validate the pull-request number (digits only) before use. # The downstream reusable workflow only ever *removes* the label, and only when # the PR genuinely meets a removal condition, so the worst a spoofed-but-valid -# number could achieve is removing a single, manually re-addable label from a PR +# number could achieve is removing manually re-addable review labels from a PR # that already qualified for removal. on: workflow_run: @@ -66,8 +66,12 @@ jobs: manage-label: needs: read-pr + strategy: + matrix: + label: [S-needs-review, ready-for-review] # Shared workflow in the org-wide `.github` repository, pinned to a commit SHA. uses: hyperlight-dev/.github/.github/workflows/manage-ready-for-review.yml@e5e471d927d3a72676ab48433da8a99d15a32ad7 with: + label: ${{ matrix.label }} # fromJSON turns the validated digit-string into a numeric input. pr-number: ${{ fromJSON(needs.read-pr.outputs.pr_number) }} diff --git a/Justfile b/Justfile index a05dd8f3d1..20ac4b9402 100644 --- a/Justfile +++ b/Justfile @@ -772,7 +772,7 @@ snapshot-goldens-regenerate target=default-target: mkdir -p target/snapshot-goldens cp -r "${layout}" "${dest}" -# Validate the regen-goldens label against the goldens state. +# Validate the S-needs-goldens label against the goldens state. # has_label is true when a PR carries the label. The label is set # when the current GOLDENS_VERSION was bumped and is not published. # base_ref is the git ref to diff the version against. @@ -788,17 +788,17 @@ snapshot-goldens-check-label has_label base_ref image=default-snapshot-goldens-i base_version="" git cat-file -e "{{ base_ref }}:${version_file}" 2>/dev/null && base_version=$(git show "{{ base_ref }}:${version_file}" | parse_version) if [[ "${version}" == "${base_version}" ]]; then - echo "::error::regen-goldens is set but GOLDENS_VERSION is unchanged from {{ base_ref }} (${version}). A regen accompanies a version bump. Remove the label or bump the version." >&2 + echo "::error::S-needs-goldens (or regen-goldens) is set but GOLDENS_VERSION is unchanged from {{ base_ref }} (${version}). A regen accompanies a version bump. Remove the label or bump the version." >&2 exit 1 fi if published "${version}"; then - echo "::error::regen-goldens is set but goldens for ${version} are already published. Remove the label so CI verifies against the published goldens." >&2 + echo "::error::S-needs-goldens (or regen-goldens) is set but goldens for ${version} are already published. Remove the label so CI verifies against the published goldens." >&2 exit 1 fi echo "snapshot-goldens-check-label: OK. ${version} bumped from ${base_version} and not yet published." else if ! published "${version}"; then - echo "::error::goldens for ${version} are not published. Add the regen-goldens label, or run RegenSnapshotGoldens first." >&2 + echo "::error::goldens for ${version} are not published. Add S-needs-goldens, or run RegenSnapshotGoldens first." >&2 exit 1 fi echo "snapshot-goldens-check-label: OK. ${version} is published." diff --git a/dev/check-pr-labels.jq b/dev/check-pr-labels.jq new file mode 100644 index 0000000000..60fb4e89d6 --- /dev/null +++ b/dev/check-pr-labels.jq @@ -0,0 +1,12 @@ +.labels | map(.name) as $labels | +[$labels[] | . as $label | + if startswith("kind/") and . != "kind/documentation" then + ([$aliases[0] | to_entries[] | + select(.value | index($label)) | .key][0] // $label) + elif startswith("C-") then . + else empty end +] | unique | length as $count | +if $count == 1 then true +elif $count == 0 and ($labels | index("kind/documentation")) != null then true +else error("PR must have exactly one C- category, found \($count). Documentation also needs a category.") +end diff --git a/dev/check-release-blockers.sh b/dev/check-release-blockers.sh index cf7475c656..f47ee10717 100755 --- a/dev/check-release-blockers.sh +++ b/dev/check-release-blockers.sh @@ -5,7 +5,7 @@ set -o pipefail ## DESCRIPTION: ## -## This script checks for open issues with the 'release-blocker' label +## This script checks for open issues with P-release-blocker or release-blocker ## in a given GitHub repository. It exits with code 1 if any blocking ## issues are found, or 0 if none are found. ## @@ -28,17 +28,24 @@ if [ -z "${1:-}" ]; then fi REPO="$1" -echo "Checking for open issues with 'release-blocker' label in $REPO..." +echo "Checking for open issues with P-release-blocker or release-blocker in $REPO..." # Extract owner and repo name from the argument OWNER=$(echo "$REPO" | cut -d'/' -f1) REPO_NAME=$(echo "$REPO" | cut -d'/' -f2) -# Get all open issues with release-blocker label BLOCKING_ISSUES=$(gh api graphql -f query=' query($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { - issues(first: 100, states: OPEN, labels: ["release-blocker"]) { + legacy: issues(first: 100, states: OPEN, labels: ["release-blocker"]) { + totalCount + nodes { + number + title + url + } + } + structured: issues(first: 100, states: OPEN, labels: ["P-release-blocker"]) { totalCount nodes { number @@ -47,15 +54,15 @@ BLOCKING_ISSUES=$(gh api graphql -f query=' } } } - }' -f owner="$OWNER" -f repo="$REPO_NAME" --jq '.data.repository.issues') + }' -f owner="$OWNER" -f repo="$REPO_NAME" --jq '.data.repository') -BLOCKER_COUNT=$(echo "$BLOCKING_ISSUES" | jq '.totalCount') +BLOCKER_COUNT=$(echo "$BLOCKING_ISSUES" | jq -e '.legacy.totalCount + .structured.totalCount') if [ "$BLOCKER_COUNT" -gt 0 ]; then - echo "❌ Found $BLOCKER_COUNT open release-blocking issue(s):" - echo "$BLOCKING_ISSUES" | jq -r '.nodes[] | " - #\(.number): \(.title) (\(.url))"' + echo "❌ Found open release-blocking issues ($BLOCKER_COUNT label matches):" + echo "$BLOCKING_ISSUES" | jq -r '[.legacy.nodes[], .structured.nodes[]] | unique_by(.number)[] | " - #\(.number): \(.title) (\(.url))"' echo "" - echo "Release blocked by open issue(s) with 'release-blocker' label" + echo "Release blocked by P-release-blocker or release-blocker issues" exit 1 else echo "✅ No open release blocking issues found" diff --git a/dev/ensure-issue-status.sh b/dev/ensure-issue-status.sh new file mode 100644 index 0000000000..a9a90bf969 --- /dev/null +++ b/dev/ensure-issue-status.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -euo pipefail + +source "$(dirname "$0")/label-utils.sh" +REPO="${GITHUB_REPOSITORY:?}" +ISSUE_NUMBER="${1:?}" +LABELS="$(gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json labels)" +HAS_STATUS="$(jq -r 'any(.labels[].name; startswith("S-") or startswith("lifecycle/"))' <<<"$LABELS")" +if [ "$HAS_STATUS" = true ]; then + exit 0 +fi +load_available_labels +STATUS="$(resolve_label S-needs-triage)" +gh issue edit "$ISSUE_NUMBER" --repo "$REPO" --add-label "$STATUS" diff --git a/dev/label-aliases.json b/dev/label-aliases.json new file mode 100644 index 0000000000..de7e60f078 --- /dev/null +++ b/dev/label-aliases.json @@ -0,0 +1,15 @@ +{ + "A-ci-periodics": ["area/ci-periodics"], + "A-ci-periodics-aarch64": ["area/ci-periodics-aarch64"], + "A-ci-periodics-mshv-6.18": ["area/ci-periodics-mshv-6.18"], + "A-ci-periodics-nightly": ["area/ci-periodics-nightly"], + "A-fuzzing": ["area/fuzzing"], + "A-perf": ["area/performance", "area/benchmarks"], + "A-testing": ["area/testing"], + "C-bug": ["kind/bugfix", "kind/bug", "bug"], + "C-deps": ["kind/dependencies"], + "C-enhancement": ["kind/enhancement"], + "C-refactor": ["kind/refactor"], + "P-release-blocker": ["release-blocker"], + "S-needs-triage": ["lifecycle/needs-review"] +} diff --git a/dev/label-utils.sh b/dev/label-utils.sh new file mode 100644 index 0000000000..e00d1fa158 --- /dev/null +++ b/dev/label-utils.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +LABEL_ALIASES="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/label-aliases.json" + +# Prefer the structured name, or an existing alias during rollout. +resolve_label() { + local label="$1" + jq -er --arg label "$label" --argjson available "$AVAILABLE_LABELS" ' + ([$label] + (.[$label] // [])) as $candidates | + [$candidates[] | select(. as $name | $available | index($name))][0] // + error("No available label for " + $label) + ' "$LABEL_ALIASES" +} + +load_available_labels() { + AVAILABLE_LABELS="$(gh api "repos/$REPO/labels" --paginate --slurp | + jq -e '[.[][] | .name]')" +} diff --git a/dev/notify-ci-failure.sh b/dev/notify-ci-failure.sh index 69be564c1b..282e53dda1 100755 --- a/dev/notify-ci-failure.sh +++ b/dev/notify-ci-failure.sh @@ -19,17 +19,18 @@ set -o pipefail ## or that the user is logged into the gh cli with an account with those permissions ## ## Usage examples: -## ./dev/notify-fuzzing-failure.sh -## ./dev/notify-fuzzing-failure.sh --title="Nightly Failure" --labels="area/testing,kind/bug" -## ./dev/notify-fuzzing-failure.sh --test +## ./dev/notify-ci-failure.sh +## ./dev/notify-ci-failure.sh --title="Nightly Failure" --labels="A-testing,C-bug" +## ./dev/notify-ci-failure.sh --test ## Run this script locally like: -## GITHUB_REPOSITORY="fork/hyperlight" GITHUB_RUN_ID=1 ./dev/notify-fuzzing-failure.sh --title="Nightly Failure" --labels="area/testing,kind/bug" +## GITHUB_REPOSITORY="fork/hyperlight" GITHUB_RUN_ID=1 ./dev/notify-ci-failure.sh --title="Nightly Failure" --labels="A-testing,C-bug" REPO="${GITHUB_REPOSITORY:-hyperlight-dev/hyperlight}" WORKFLOW_RUN_URL="${GITHUB_SERVER_URL:-https://github.com}/${REPO}/actions/runs/${GITHUB_RUN_ID:-unknown}" TEST_MODE=false ISSUE_TITLE="" -LABELS="area/testing,kind/bug,area/fuzzing,lifecycle/needs-review" +LABELS="A-testing,C-bug,A-fuzzing,S-needs-triage" +source "$(dirname "$0")/label-utils.sh" for arg in "$@"; do case $arg in @@ -51,9 +52,13 @@ done # Normalize labels into an array IFS=',' read -r -a LABEL_ARRAY <<< "$LABELS" +if [ "${#LABEL_ARRAY[@]}" -eq 0 ]; then + echo "At least one CI failure label is required" >&2 + exit 1 +fi # Choose a label to search existing issues for; prefer the first label if present -SEARCH_LABEL="${LABEL_ARRAY[0]:-area/fuzzing}" +SEARCH_LABEL="${LABEL_ARRAY[0]:-A-fuzzing}" # Build issue title if not provided if [ -z "$ISSUE_TITLE" ]; then @@ -75,8 +80,19 @@ fi OWNER=$(echo "$REPO" | cut -d'/' -f1) REPO_NAME=$(echo "$REPO" | cut -d'/' -f2) -echo "Checking for existing issues in $REPO with label '$SEARCH_LABEL'..." -EXISTING_ISSUES=$(gh api graphql -f query=' +load_available_labels +LABEL_ARGS=() +for lbl in "${LABEL_ARRAY[@]}"; do + RESOLVED_LABEL="$(resolve_label "$lbl")" + LABEL_ARGS+=("--label" "$RESOLVED_LABEL") +done + +# Search aliases independently. A multi-label GraphQL filter requires all labels. +SEARCH_LABELS="$(jq -r --arg label "$SEARCH_LABEL" '[$label] + (.[$label] // []) | .[]' "$LABEL_ALIASES")" +EXISTING_ISSUES='[]' +while IFS= read -r label; do + echo "Checking for existing issues in $REPO with label '$label'..." + MATCHES=$(gh api graphql -f query=' query($owner: String!, $repo: String!, $label: String!) { repository(owner: $owner, name: $repo) { issues(first: 10, states: OPEN, labels: [$label]) { @@ -93,23 +109,33 @@ EXISTING_ISSUES=$(gh api graphql -f query=' } } } - }' -f owner="$OWNER" -f repo="$REPO_NAME" -f label="$SEARCH_LABEL" --jq '.data.repository.issues') || EXISTING_ISSUES="" + }' -f owner="$OWNER" -f repo="$REPO_NAME" -f label="$label" --jq '.data.repository.issues.nodes') + EXISTING_ISSUES="$(jq -en --argjson existing "$EXISTING_ISSUES" --argjson matches "$MATCHES" ' + if ($matches | type) != "array" then error("Invalid issue search response") + else $existing + $matches | unique_by(.number) end')" +done <<<"$SEARCH_LABELS" -FUZZING_ISSUES=$(echo "$EXISTING_ISSUES" | jq '.nodes[]' 2>/dev/null || echo "") -FUZZING_ISSUE_COUNT=0 -if [ -n "$FUZZING_ISSUES" ]; then - FUZZING_ISSUE_COUNT=$(echo "$FUZZING_ISSUES" | jq -s 'length' 2>/dev/null || echo "0") -fi +FUZZING_ISSUE_COUNT=$(echo "$EXISTING_ISSUES" | jq 'length') echo "Found $FUZZING_ISSUE_COUNT existing issue(s) matching label '$SEARCH_LABEL'" if [ "$FUZZING_ISSUE_COUNT" -gt 0 ]; then - ISSUE_NUMBER=$(echo "$FUZZING_ISSUES" | jq -r '.number' | head -1) - ISSUE_URL=$(echo "$FUZZING_ISSUES" | jq -r '.url' | head -1) - if [ "$ISSUE_NUMBER" = "null" ] || [ -z "$ISSUE_NUMBER" ]; then - echo "⚠️ Could not parse issue number from search results; will create a new issue" - FUZZING_ISSUE_COUNT=0 + ISSUE_NUMBER=$(echo "$EXISTING_ISSUES" | jq -r '.[0].number') + ISSUE_URL=$(echo "$EXISTING_ISSUES" | jq -r '.[0].url') + if [[ ! "$ISSUE_NUMBER" =~ ^[0-9]+$ ]]; then + echo "Could not parse issue number from search results" >&2 + exit 1 else + EDIT_LABEL_ARGS=() + for lbl in "${LABEL_ARRAY[@]}"; do + if [ "$lbl" = "P-release-blocker" ]; then + RESOLVED_LABEL="$(resolve_label "$lbl")" + EDIT_LABEL_ARGS+=("--add-label" "$RESOLVED_LABEL") + fi + done + if [ "${#EDIT_LABEL_ARGS[@]}" -gt 0 ]; then + gh issue edit "$ISSUE_NUMBER" --repo "$REPO" "${EDIT_LABEL_ARGS[@]}" + fi echo "Adding comment to existing issue #$ISSUE_NUMBER" COMMENT_BODY="## Job Failed Again @@ -118,13 +144,9 @@ if [ "$FUZZING_ISSUE_COUNT" -gt 0 ]; then The scheduled job has failed again. Please check the workflow logs and artifacts for details." - if gh issue comment "$ISSUE_NUMBER" --body "$COMMENT_BODY" --repo "$REPO"; then - echo "✅ Added comment to existing issue #$ISSUE_NUMBER: $ISSUE_URL" - exit 0 - else - echo "❌ Failed to add comment to existing issue. Will attempt to create a new issue instead." - FUZZING_ISSUE_COUNT=0 - fi + gh issue comment "$ISSUE_NUMBER" --body "$COMMENT_BODY" --repo "$REPO" + echo "✅ Added comment to existing issue #$ISSUE_NUMBER: $ISSUE_URL" + exit 0 fi fi @@ -148,12 +170,6 @@ The scheduled job failed during execution. This issue was automatically created --- *This issue was automatically created by the CI failure notification system.*" - # Build label args for gh issue create - LABEL_ARGS=() - for lbl in "${LABEL_ARRAY[@]}"; do - LABEL_ARGS+=("--label" "$lbl") - done - if ISSUE_URL=$(gh issue create \ --title "$ISSUE_TITLE" \ --body "$ISSUE_BODY" \ diff --git a/dev/test-label-migration.py b/dev/test-label-migration.py new file mode 100644 index 0000000000..4e4426f1f7 --- /dev/null +++ b/dev/test-label-migration.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +import json +import os +from pathlib import Path +import re +import subprocess +import tempfile +import textwrap +import unittest + + +ROOT = Path(__file__).resolve().parent.parent +ALIASES = json.loads((ROOT / "dev/label-aliases.json").read_text()) +MOCK_GH = """#!/usr/bin/env python3 +import json +import os +import sys + +args = sys.argv[1:] +with open(os.environ["MOCK_CALLS"], "a") as log: + log.write(json.dumps(args) + "\\n") +data = json.loads(os.environ["MOCK_DATA"]) +if args[0] == "api": + if data.get("api_failure"): + sys.exit("Mock API failure") + if args[1] != "graphql": + print(json.dumps([[{"name": name} for name in data["available"]]])) + elif "legacy: issues" in args[args.index("-f") + 1]: + print(json.dumps(data["blockers"])) + else: + label = next(a[6:] for a in args if a.startswith("label=")) + print(json.dumps(data.get("matches", {}).get(label, []))) +elif args[:2] == ["issue", "view"]: + if data.get("invalid_issue"): + print("null") + sys.exit(0) + print(json.dumps({"labels": [{"name": n} for n in data.get("labels", [])]})) +elif args[:2] == ["issue", "create"]: + print("https://example.invalid/issues/42") +elif args[:2] in (["issue", "edit"], ["issue", "comment"]): + if data.get("write_failure"): + sys.exit("Mock write failure") +else: + sys.exit("Unexpected mock gh call: " + repr(args)) +""" + + +class LabelMigrationTests(unittest.TestCase): + def run_script(self, script, data, *args): + with tempfile.TemporaryDirectory() as directory: + directory = Path(directory) + gh = directory / "gh" + gh.write_text(MOCK_GH) + gh.chmod(0o755) + calls = directory / "calls" + env = dict( + os.environ, + PATH=f"{directory}{os.pathsep}{os.environ['PATH']}", + GITHUB_REPOSITORY="test/repo", + MOCK_DATA=json.dumps(data), + MOCK_CALLS=str(calls), + GITHUB_OUTPUT=str(directory / "output"), + ) + result = subprocess.run( + ["bash", "-c", script, "test", *args], + cwd=ROOT, env=env, text=True, capture_output=True, + ) + recorded = [json.loads(line) for line in calls.read_text().splitlines()] if calls.exists() else [] + return result, recorded + + def workflow_script(self, name): + source = (ROOT / f".github/workflows/{name}.yml").read_text() + script = re.search(r"run: \|\n(.*?)(?=\n\s+env:)", source, re.S).group(1) + return textwrap.dedent(script).replace("${{ github.event.issue.number }}", "42") + + def category(self, labels): + return subprocess.run( + ["jq", "-e", "--slurpfile", "aliases", "dev/label-aliases.json", + "-f", "dev/check-pr-labels.jq"], + input=json.dumps({"labels": [{"name": n} for n in labels]}), + cwd=ROOT, text=True, capture_output=True, + ).returncode + + def test_category_aliases_count_once(self): + for new in ("C-bug", "C-deps", "C-enhancement", "C-refactor"): + old = ALIASES[new][0] + for labels in ([old], [new], [new, old], [new, old, "A-docs"]): + with self.subTest(labels=labels): + self.assertEqual(self.category(labels), 0) + + def test_documentation_needs_category_with_legacy_grace(self): + for labels in (["kind/documentation"], ["A-docs", "C-bug"], + ["kind/documentation", "C-enhancement"]): + self.assertEqual(self.category(labels), 0) + for labels in ([], ["A-docs"], ["C-bug", "C-deps"], + ["kind/documentation", "C-bug", "C-refactor"]): + self.assertNotEqual(self.category(labels), 0) + + def test_multiple_statuses_are_preserved(self): + for labels in (["S-needs-review", "S-nominated"], + ["lifecycle/confirmed", "S-blocked"], + ["lifecycle/confirmed", "lifecycle/blocked"]): + result, calls = self.run_script( + 'bash dev/ensure-issue-status.sh 42', {"labels": labels}) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(len(calls), 1) + + def test_status_default_prefers_new_and_falls_back(self): + for available, expected in ( + (["S-needs-triage", "lifecycle/needs-review"], "S-needs-triage"), + (["lifecycle/needs-review"], "lifecycle/needs-review"), + ): + result, calls = self.run_script( + 'bash dev/ensure-issue-status.sh 42', {"available": available}) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(calls[-1][-2:], ["--add-label", expected]) + + def test_missing_status_fails(self): + result, calls = self.run_script( + 'bash dev/ensure-issue-status.sh 42', {"available": []}) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(any(call[:2] == ["issue", "edit"] for call in calls)) + + def test_release_blockers_either_name_and_both(self): + issue = {"number": 42, "title": "Failure", "url": "https://example.invalid/42"} + for old, new in ((0, 0), (1, 0), (0, 1), (1, 1), (0, 101)): + data = {"blockers": { + "legacy": {"totalCount": old, "nodes": [issue] if old else []}, + "structured": {"totalCount": new, "nodes": [issue] if new else []}, + }} + result, calls = self.run_script('bash dev/check-release-blockers.sh test/repo', data) + self.assertEqual(result.returncode, 1 if old or new else 0, result.stderr) + query = calls[0][calls[0].index("-f") + 1] + self.assertIn('labels: ["release-blocker"]', query) + self.assertIn('labels: ["P-release-blocker"]', query) + if old or new: + self.assertEqual(result.stdout.count("#42:"), 1) + + def test_blocker_cleanup_removes_both(self): + for labels in ([], ["P-release-blocker"], ["release-blocker"], + ["P-release-blocker", "release-blocker"]): + result, calls = self.run_script( + self.workflow_script("ReleaseBlockerLabelCleanUp"), {"labels": labels}) + self.assertEqual(result.returncode, 0, result.stderr) + edits = [call[-1] for call in calls if call[:2] == ["issue", "edit"]] + self.assertCountEqual(edits, labels) + + def notifier(self, data, labels="A-perf,A-testing,S-needs-triage,P-release-blocker"): + return self.run_script( + 'bash dev/notify-ci-failure.sh "$@"', data, f"--labels={labels}") + + def test_notifier_existing_aliases_remain_discoverable(self): + issue = {"number": 42, "url": "https://example.invalid/42"} + requested = ["A-perf", "A-testing", "S-needs-triage", "P-release-blocker"] + for available in (requested, [ALIASES[n][0] for n in requested], + requested + [ALIASES[n][0] for n in requested]): + for key in ["A-perf"] + ALIASES["A-perf"]: + result, calls = self.notifier({ + "available": available, "matches": {key: [issue]}}) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertFalse(any(call[:2] == ["issue", "create"] for call in calls)) + edit = next(call for call in calls if call[:2] == ["issue", "edit"]) + expected = "P-release-blocker" if "P-release-blocker" in available else "release-blocker" + self.assertEqual(edit[-2:], ["--add-label", expected]) + + def test_notifier_creates_with_resolved_labels(self): + for requested in ( + ["A-testing", "C-bug", "A-fuzzing", "S-needs-triage"], + ["A-ci-periodics", "A-testing", "S-needs-triage"], + ["A-ci-periodics-aarch64", "A-ci-periodics", "A-testing", "S-needs-triage", "P-release-blocker"], + ["A-ci-periodics-nightly", "A-ci-periodics", "A-testing", "S-needs-triage", "P-release-blocker"], + ["A-ci-periodics-mshv-6.18", "A-ci-periodics", "A-testing", "S-needs-triage", "P-release-blocker"], + ["A-fuzzing", "A-testing", "S-needs-triage", "P-release-blocker"], + ): + for available in (requested, [ALIASES[n][0] for n in requested]): + result, calls = self.notifier({"available": available}, ",".join(requested)) + self.assertEqual(result.returncode, 0, result.stderr) + create = next(call for call in calls if call[:2] == ["issue", "create"]) + self.assertEqual(create[create.index("--label") + 1:create.index("--repo"):2], available) + + def test_notifier_failures_never_create_duplicate(self): + issue = {"number": 42, "url": "https://example.invalid/42"} + for data in ( + {"available": []}, + {"api_failure": True}, + {"available": ["A-perf"], "api_failure": True}, + {"available": ["A-perf"], "matches": {"area/benchmarks": [issue]}, "write_failure": True}, + {"available": ["A-perf"], "matches": {"A-perf": None}}, + ): + result, calls = self.notifier(data, "A-perf") + self.assertNotEqual(result.returncode, 0) + self.assertFalse(any(call[:2] == ["issue", "create"] for call in calls)) + + def test_read_failures_fail_release_and_status(self): + for script in ('bash dev/check-release-blockers.sh test/repo', + 'bash dev/ensure-issue-status.sh 42'): + result, _ = self.run_script(script, {"api_failure": True}) + self.assertNotEqual(result.returncode, 0) + + def test_invalid_issue_read_fails_without_edits(self): + for script in ('bash dev/ensure-issue-status.sh 42', + self.workflow_script("ReleaseBlockerLabelCleanUp")): + result, calls = self.run_script(script, {"invalid_issue": True}) + self.assertNotEqual(result.returncode, 0) + self.assertFalse(any(call[:2] == ["issue", "edit"] for call in calls)) + + def test_empty_notifier_labels_fail_without_calls(self): + result, calls = self.notifier({}, "") + self.assertNotEqual(result.returncode, 0) + self.assertEqual(calls, []) + + def test_existing_failure_status_is_not_reset(self): + result, calls = self.notifier({ + "available": ["A-perf", "S-needs-triage"], + "matches": {"area/performance": [{"number": 42, "url": "https://example.invalid/42"}]}, + }, "A-perf,S-needs-triage") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertFalse(any(call[:2] == ["issue", "edit"] for call in calls)) + + def test_golden_mode_accepts_either_label(self): + workflow = (ROOT / ".github/workflows/ValidatePullRequest.yml").read_text() + pattern = re.search(r"grep -Eqx '([^']+)'", workflow).group(1) + for labels, expected in ( + ("", 1), ("A-docs", 1), ("regen-goldens", 0), + ("S-needs-goldens", 0), ("regen-goldens\nS-needs-goldens", 0), + ): + result = subprocess.run( + ["grep", "-Eqx", pattern], input=labels + "\n", text=True) + self.assertEqual(result.returncode, expected) + + def test_static_producers_and_consumers_keep_aliases(self): + dependabot = (ROOT / ".github/dependabot.yml").read_text() + self.assertEqual(dependabot.count('"C-deps"'), 3) + self.assertEqual(dependabot.count('"kind/dependencies"'), 3) + self.assertNotIn("area/guest", dependabot) + release = (ROOT / ".github/release.yml").read_text() + self.assertEqual(release.count("C-deps"), 2) + self.assertEqual(release.count("kind/dependencies"), 2) + manage = (ROOT / ".github/workflows/ready-for-review-manage.yml").read_text() + self.assertIn("label: [S-needs-review, ready-for-review]", manage) + self.assertIn("label: ${{ matrix.label }}", manage) + self.assertIn('workflows: ["Ready-for-review (collect)"]', manage) + self.assertIn("name: ready-for-review-pr-number", manage) + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/how-to-make-releases.md b/docs/how-to-make-releases.md index 4d377c7e44..8f033aff92 100644 --- a/docs/how-to-make-releases.md +++ b/docs/how-to-make-releases.md @@ -4,7 +4,12 @@ This document details the process of releasing a new version of Hyperlight to [c ## Resolve any release blockers -Before starting the release process, check for open issues labeled `release-blocker` in the [issue tracker](https://github.com/hyperlight-dev/hyperlight/issues?q=is%3Aissue+is%3Aopen+label%3Arelease-blocker). All such issues must be resolved (closed) before proceeding — the ["Create a Release"](https://github.com/hyperlight-dev/hyperlight/actions/workflows/CreateRelease.yml) workflow runs a [release blocker check](https://github.com/hyperlight-dev/hyperlight/actions/workflows/ReleaseBlockerCheck.yml) as its first step, and all subsequent jobs depend on it, so the publish will fail if any `release-blocker` issues remain open. +Before releasing, resolve all open issues labeled `P-release-blocker` or its +rollout alias `release-blocker`. The [issue tracker](https://github.com/hyperlight-dev/hyperlight/issues?q=is%3Aissue+is%3Aopen+label%3AP-release-blocker%2Crelease-blocker) +search includes both names. The ["Create a Release"](https://github.com/hyperlight-dev/hyperlight/actions/workflows/CreateRelease.yml) +workflow runs a [release blocker check](https://github.com/hyperlight-dev/hyperlight/actions/workflows/ReleaseBlockerCheck.yml) +first. It blocks release-branch publishes if either label appears on an open +issue. Releases from other branches skip this check. ## Update Cargo.toml Versions @@ -77,4 +82,3 @@ When this job is done, a new [GitHub release](https://github.com/hyperlight-dev/ 5. Cherry-pick the commits from `main` that you want to include in the patch (and resolve any conflicts). You must include the commit that updated the `CHANGELOG.md` and the commit that updated the `Cargo.toml` versions. 6. Create a tag for the patch release, e.g. `v0.4.1`, similar to the steps above. Push the tag. A job will start to try to make a new release branch for you, but it will fail because the `release/v0.4.1` branch already exists. This is expected, so don't worry about it. 7. Follow the steps above to create a new GitHub release, but this time select the `release/v0.4.1` branch in the "Use workflow from" dropdown. This will create a new GitHub release for you, and publish the updated packages to crates.io. - diff --git a/docs/snapshot-versioning.md b/docs/snapshot-versioning.md index 1a92a423bc..b1af1092ad 100644 --- a/docs/snapshot-versioning.md +++ b/docs/snapshot-versioning.md @@ -140,7 +140,7 @@ Steps: format, dispatching on `abi_version`. 5. Update the tripwire assertions in `tripwires.rs` and any affected tests to match the new values. -6. Bump `GOLDENS_VERSION` to the next major. Apply the `regen-goldens` +6. Bump `GOLDENS_VERSION` to the next major. Apply the `S-needs-goldens` label to the pull request so the verify job regenerates against the branch. See [Breaking the format on a pull request](#breaking-the-format-on-a-pull-request) @@ -168,7 +168,7 @@ Steps: 3. Bump `SNAPSHOT_ABI_VERSION`. 4. Update the tripwire assertions in `tripwires.rs` and any affected tests to match the new values. -5. Bump `GOLDENS_VERSION` to the next major. Apply the `regen-goldens` +5. Bump `GOLDENS_VERSION` to the next major. Apply the `S-needs-goldens` label to the pull request so the verify job regenerates against the branch. See [Breaking the format on a pull request](#breaking-the-format-on-a-pull-request) @@ -187,7 +187,7 @@ loads the tag `{GOLDENS_VERSION}-{arch}-{hv}-{cpu}-{profile}` from a local directory that `just snapshot-goldens-pull` populates from GHCR. A freshly bumped `GOLDENS_VERSION` has no tags on GHCR until the bump merges to `main` and the publish workflow runs, so pull requests that -bump the version verify through the `regen-goldens` label instead (see +bump the version verify through the `S-needs-goldens` label instead (see [Breaking the format on a pull request](#breaking-the-format-on-a-pull-request)). A snapshot is bound to its CPU vendor. The loader rejects a snapshot @@ -235,7 +235,7 @@ frozen from then on. The publish workflow only publishes a version whose completion marker is absent from GHCR, so a published baseline cannot be clobbered by a later run. While a developer iterates on a v1 to v2 bump the new version is unpublished, so they verify locally with -`just snapshot-goldens-generate` and the `regen-goldens` label rather +`just snapshot-goldens-generate` and the `S-needs-goldens` label rather than pushing to GHCR. The freeze is enforced by the publish workflow's marker check, not by a @@ -252,15 +252,17 @@ corrupted push. A pull request that bumps `GOLDENS_VERSION` introduces a tag set that GHCR does not carry yet, so the default pull-and-verify path has nothing -to load. The `regen-goldens` label switches the verify job into +to load. The `S-needs-goldens` label switches the verify job into regenerate mode for that pull request. +CI also accepts `regen-goldens` during the label rename. + * **Without the label**, the job pulls the published tag set for the current `GOLDENS_VERSION` and verifies it against the branch. Missing tags fail the job. This is what turns an accidental format break into a red build: the published bytes stop loading, and the author must either restructure the change or own the break with the label. -* **With the `regen-goldens` label**, the job generates the current +* **With the `S-needs-goldens` label**, the job generates the current golden from the branch source, pulls the `COMPAT_VERSIONS` goldens from the registry, and runs both through the branch loader. This proves the new format is internally loadable on each runner and that @@ -306,7 +308,7 @@ with `force: true` republishes a version that already exists, reserved for recovering a corrupted or partial push. The push-triggered publish closes the window in which a pull request -that bumped the version needs the `regen-goldens` label. Once `main` +that bumped the version needs the `S-needs-goldens` label. Once `main` carries the bump and the publish lands, new pull requests pass on the default pull-and-verify path. @@ -344,7 +346,7 @@ Steps: 2. Bump `GOLDENS_VERSION` minor (e.g. `v1.2` to `v1.3`). The new prefix has no published tags, so the default verify path fails until they exist. -3. Apply the `regen-goldens` label to the pull request. The verify job +3. Apply the `S-needs-goldens` label to the pull request. The verify job regenerates the full check set against the branch and runs it back through the branch loader. See [Breaking the format on a pull request](#breaking-the-format-on-a-pull-request). From 90a00d624d4cf3b364028b997bbf8636e36212ee Mon Sep 17 00:00:00 2001 From: Yosh Date: Tue, 10 Nov 2026 13:14:39 +0100 Subject: [PATCH 02/14] Keep label migration in existing workflows and scripts Signed-off-by: Yosh Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/DailyArm64.yml | 3 +- .github/workflows/IssueLabelChecker.yml | 16 +- .github/workflows/PRLabelChecker.yml | 9 +- dev/check-pr-labels.jq | 12 -- dev/check-release-blockers.sh | 4 +- dev/ensure-issue-status.sh | 14 -- dev/label-aliases.json | 15 -- dev/label-utils.sh | 18 -- dev/notify-ci-failure.sh | 62 +++--- dev/test-label-migration.py | 247 ------------------------ 10 files changed, 49 insertions(+), 351 deletions(-) delete mode 100644 dev/check-pr-labels.jq delete mode 100644 dev/ensure-issue-status.sh delete mode 100644 dev/label-aliases.json delete mode 100644 dev/label-utils.sh delete mode 100644 dev/test-label-migration.py diff --git a/.github/workflows/DailyArm64.yml b/.github/workflows/DailyArm64.yml index c8411c6701..8ff17d4b3e 100644 --- a/.github/workflows/DailyArm64.yml +++ b/.github/workflows/DailyArm64.yml @@ -103,8 +103,7 @@ jobs: # File (or update) a release-blocking GitHub issue if any job fails. The first # label (A-ci-periodics-aarch64) keeps these failures on a dedicated issue. - # The notifier accepts an existing rollout alias and ensures the issue - # carries the release blocker label. + # The notifier accepts the existing name during the label rename. notify-failure: runs-on: ubuntu-latest needs: [build-guests, miri, build-test, run-examples, fuzzing] diff --git a/.github/workflows/IssueLabelChecker.yml b/.github/workflows/IssueLabelChecker.yml index 40ddadb0f6..f66a2267ad 100644 --- a/.github/workflows/IssueLabelChecker.yml +++ b/.github/workflows/IssueLabelChecker.yml @@ -14,7 +14,17 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Check and Add label run: | - bash dev/ensure-issue-status.sh "${{ github.event.issue.number }}" + set -euo pipefail + HAS_STATUS="$(gh issue view ${{ github.event.issue.number }} --json labels \ + -q 'any(.labels[].name; startswith("S-") or startswith("lifecycle/"))')" + if [[ "$HAS_STATUS" == "false" ]]; then + LABELS="$(gh label list --limit 1000 --json name)" + STATUS="$(jq -er ' + [.[].name] | + if index("S-needs-triage") then "S-needs-triage" + elif index("lifecycle/needs-review") then "lifecycle/needs-review" + else error("Missing triage label") end' <<<"$LABELS")" + gh issue edit ${{ github.event.issue.number }} --add-label "$STATUS" + fi env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_REPOSITORY: ${{ github.repository }} \ No newline at end of file + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/PRLabelChecker.yml b/.github/workflows/PRLabelChecker.yml index 8ed76079bc..c8ab036f25 100644 --- a/.github/workflows/PRLabelChecker.yml +++ b/.github/workflows/PRLabelChecker.yml @@ -12,13 +12,14 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Test label migration offline - run: python3 dev/test-label-migration.py - name: Ensure exactly one category is applied run: | set -euo pipefail PR_NUMBER=${{ github.event.pull_request.number }} - gh pr view "$PR_NUMBER" --json labels | - jq -e --slurpfile aliases dev/label-aliases.json -f dev/check-pr-labels.jq + # Accept legacy categories until the labels are renamed. + gh pr view "$PR_NUMBER" --json labels | jq -e ' + [.labels[].name | select(startswith("C-") or startswith("kind/"))] | + if length == 1 then true + else error("PR must have exactly one category label") end' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/dev/check-pr-labels.jq b/dev/check-pr-labels.jq deleted file mode 100644 index 60fb4e89d6..0000000000 --- a/dev/check-pr-labels.jq +++ /dev/null @@ -1,12 +0,0 @@ -.labels | map(.name) as $labels | -[$labels[] | . as $label | - if startswith("kind/") and . != "kind/documentation" then - ([$aliases[0] | to_entries[] | - select(.value | index($label)) | .key][0] // $label) - elif startswith("C-") then . - else empty end -] | unique | length as $count | -if $count == 1 then true -elif $count == 0 and ($labels | index("kind/documentation")) != null then true -else error("PR must have exactly one C- category, found \($count). Documentation also needs a category.") -end diff --git a/dev/check-release-blockers.sh b/dev/check-release-blockers.sh index f47ee10717..b7f31a5d9f 100755 --- a/dev/check-release-blockers.sh +++ b/dev/check-release-blockers.sh @@ -59,8 +59,8 @@ BLOCKING_ISSUES=$(gh api graphql -f query=' BLOCKER_COUNT=$(echo "$BLOCKING_ISSUES" | jq -e '.legacy.totalCount + .structured.totalCount') if [ "$BLOCKER_COUNT" -gt 0 ]; then - echo "❌ Found open release-blocking issues ($BLOCKER_COUNT label matches):" - echo "$BLOCKING_ISSUES" | jq -r '[.legacy.nodes[], .structured.nodes[]] | unique_by(.number)[] | " - #\(.number): \(.title) (\(.url))"' + echo "❌ Found $BLOCKER_COUNT open release-blocking issue(s):" + echo "$BLOCKING_ISSUES" | jq -r '.legacy.nodes[], .structured.nodes[] | " - #\(.number): \(.title) (\(.url))"' echo "" echo "Release blocked by P-release-blocker or release-blocker issues" exit 1 diff --git a/dev/ensure-issue-status.sh b/dev/ensure-issue-status.sh deleted file mode 100644 index a9a90bf969..0000000000 --- a/dev/ensure-issue-status.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -set -euo pipefail - -source "$(dirname "$0")/label-utils.sh" -REPO="${GITHUB_REPOSITORY:?}" -ISSUE_NUMBER="${1:?}" -LABELS="$(gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json labels)" -HAS_STATUS="$(jq -r 'any(.labels[].name; startswith("S-") or startswith("lifecycle/"))' <<<"$LABELS")" -if [ "$HAS_STATUS" = true ]; then - exit 0 -fi -load_available_labels -STATUS="$(resolve_label S-needs-triage)" -gh issue edit "$ISSUE_NUMBER" --repo "$REPO" --add-label "$STATUS" diff --git a/dev/label-aliases.json b/dev/label-aliases.json deleted file mode 100644 index de7e60f078..0000000000 --- a/dev/label-aliases.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "A-ci-periodics": ["area/ci-periodics"], - "A-ci-periodics-aarch64": ["area/ci-periodics-aarch64"], - "A-ci-periodics-mshv-6.18": ["area/ci-periodics-mshv-6.18"], - "A-ci-periodics-nightly": ["area/ci-periodics-nightly"], - "A-fuzzing": ["area/fuzzing"], - "A-perf": ["area/performance", "area/benchmarks"], - "A-testing": ["area/testing"], - "C-bug": ["kind/bugfix", "kind/bug", "bug"], - "C-deps": ["kind/dependencies"], - "C-enhancement": ["kind/enhancement"], - "C-refactor": ["kind/refactor"], - "P-release-blocker": ["release-blocker"], - "S-needs-triage": ["lifecycle/needs-review"] -} diff --git a/dev/label-utils.sh b/dev/label-utils.sh deleted file mode 100644 index e00d1fa158..0000000000 --- a/dev/label-utils.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -LABEL_ALIASES="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/label-aliases.json" - -# Prefer the structured name, or an existing alias during rollout. -resolve_label() { - local label="$1" - jq -er --arg label "$label" --argjson available "$AVAILABLE_LABELS" ' - ([$label] + (.[$label] // [])) as $candidates | - [$candidates[] | select(. as $name | $available | index($name))][0] // - error("No available label for " + $label) - ' "$LABEL_ALIASES" -} - -load_available_labels() { - AVAILABLE_LABELS="$(gh api "repos/$REPO/labels" --paginate --slurp | - jq -e '[.[][] | .name]')" -} diff --git a/dev/notify-ci-failure.sh b/dev/notify-ci-failure.sh index 282e53dda1..7c315b25ea 100755 --- a/dev/notify-ci-failure.sh +++ b/dev/notify-ci-failure.sh @@ -30,7 +30,6 @@ WORKFLOW_RUN_URL="${GITHUB_SERVER_URL:-https://github.com}/${REPO}/actions/runs/ TEST_MODE=false ISSUE_TITLE="" LABELS="A-testing,C-bug,A-fuzzing,S-needs-triage" -source "$(dirname "$0")/label-utils.sh" for arg in "$@"; do case $arg in @@ -52,10 +51,6 @@ done # Normalize labels into an array IFS=',' read -r -a LABEL_ARRAY <<< "$LABELS" -if [ "${#LABEL_ARRAY[@]}" -eq 0 ]; then - echo "At least one CI failure label is required" >&2 - exit 1 -fi # Choose a label to search existing issues for; prefer the first label if present SEARCH_LABEL="${LABEL_ARRAY[0]:-A-fuzzing}" @@ -80,19 +75,27 @@ fi OWNER=$(echo "$REPO" | cut -d'/' -f1) REPO_NAME=$(echo "$REPO" | cut -d'/' -f2) -load_available_labels -LABEL_ARGS=() -for lbl in "${LABEL_ARRAY[@]}"; do - RESOLVED_LABEL="$(resolve_label "$lbl")" - LABEL_ARGS+=("--label" "$RESOLVED_LABEL") +# Resolve names before and after the in-place label rename. +AVAILABLE_LABELS="$(gh api "repos/$REPO/labels" --paginate --slurp | jq -e '[.[][] | .name]')" +for i in "${!LABEL_ARRAY[@]}"; do + label="${LABEL_ARRAY[$i]}" + case "$label" in + A-perf) legacy="area/performance" ;; + A-*) legacy="area/${label#A-}" ;; + C-bug) legacy="kind/bugfix" ;; + S-needs-triage) legacy="lifecycle/needs-review" ;; + P-release-blocker) legacy="release-blocker" ;; + *) legacy="$label" ;; + esac + LABEL_ARRAY[$i]="$(jq -er --arg label "$label" --arg legacy "$legacy" ' + if index($label) then $label + elif index($legacy) then $legacy + else error("Missing label: " + $label) end' <<<"$AVAILABLE_LABELS")" done +SEARCH_LABEL="${LABEL_ARRAY[0]}" -# Search aliases independently. A multi-label GraphQL filter requires all labels. -SEARCH_LABELS="$(jq -r --arg label "$SEARCH_LABEL" '[$label] + (.[$label] // []) | .[]' "$LABEL_ALIASES")" -EXISTING_ISSUES='[]' -while IFS= read -r label; do - echo "Checking for existing issues in $REPO with label '$label'..." - MATCHES=$(gh api graphql -f query=' +echo "Checking for existing issues in $REPO with label '$SEARCH_LABEL'..." +EXISTING_ISSUES=$(gh api graphql -f query=' query($owner: String!, $repo: String!, $label: String!) { repository(owner: $owner, name: $repo) { issues(first: 10, states: OPEN, labels: [$label]) { @@ -109,33 +112,19 @@ while IFS= read -r label; do } } } - }' -f owner="$OWNER" -f repo="$REPO_NAME" -f label="$label" --jq '.data.repository.issues.nodes') - EXISTING_ISSUES="$(jq -en --argjson existing "$EXISTING_ISSUES" --argjson matches "$MATCHES" ' - if ($matches | type) != "array" then error("Invalid issue search response") - else $existing + $matches | unique_by(.number) end')" -done <<<"$SEARCH_LABELS" + }' -f owner="$OWNER" -f repo="$REPO_NAME" -f label="$SEARCH_LABEL" --jq '.data.repository.issues') -FUZZING_ISSUE_COUNT=$(echo "$EXISTING_ISSUES" | jq 'length') +FUZZING_ISSUE_COUNT=$(echo "$EXISTING_ISSUES" | jq -er '.totalCount') echo "Found $FUZZING_ISSUE_COUNT existing issue(s) matching label '$SEARCH_LABEL'" if [ "$FUZZING_ISSUE_COUNT" -gt 0 ]; then - ISSUE_NUMBER=$(echo "$EXISTING_ISSUES" | jq -r '.[0].number') - ISSUE_URL=$(echo "$EXISTING_ISSUES" | jq -r '.[0].url') + ISSUE_NUMBER=$(echo "$EXISTING_ISSUES" | jq -r '.nodes[0].number') + ISSUE_URL=$(echo "$EXISTING_ISSUES" | jq -r '.nodes[0].url') if [[ ! "$ISSUE_NUMBER" =~ ^[0-9]+$ ]]; then echo "Could not parse issue number from search results" >&2 exit 1 else - EDIT_LABEL_ARGS=() - for lbl in "${LABEL_ARRAY[@]}"; do - if [ "$lbl" = "P-release-blocker" ]; then - RESOLVED_LABEL="$(resolve_label "$lbl")" - EDIT_LABEL_ARGS+=("--add-label" "$RESOLVED_LABEL") - fi - done - if [ "${#EDIT_LABEL_ARGS[@]}" -gt 0 ]; then - gh issue edit "$ISSUE_NUMBER" --repo "$REPO" "${EDIT_LABEL_ARGS[@]}" - fi echo "Adding comment to existing issue #$ISSUE_NUMBER" COMMENT_BODY="## Job Failed Again @@ -170,6 +159,11 @@ The scheduled job failed during execution. This issue was automatically created --- *This issue was automatically created by the CI failure notification system.*" + LABEL_ARGS=() + for lbl in "${LABEL_ARRAY[@]}"; do + LABEL_ARGS+=("--label" "$lbl") + done + if ISSUE_URL=$(gh issue create \ --title "$ISSUE_TITLE" \ --body "$ISSUE_BODY" \ diff --git a/dev/test-label-migration.py b/dev/test-label-migration.py deleted file mode 100644 index 4e4426f1f7..0000000000 --- a/dev/test-label-migration.py +++ /dev/null @@ -1,247 +0,0 @@ -#!/usr/bin/env python3 -import json -import os -from pathlib import Path -import re -import subprocess -import tempfile -import textwrap -import unittest - - -ROOT = Path(__file__).resolve().parent.parent -ALIASES = json.loads((ROOT / "dev/label-aliases.json").read_text()) -MOCK_GH = """#!/usr/bin/env python3 -import json -import os -import sys - -args = sys.argv[1:] -with open(os.environ["MOCK_CALLS"], "a") as log: - log.write(json.dumps(args) + "\\n") -data = json.loads(os.environ["MOCK_DATA"]) -if args[0] == "api": - if data.get("api_failure"): - sys.exit("Mock API failure") - if args[1] != "graphql": - print(json.dumps([[{"name": name} for name in data["available"]]])) - elif "legacy: issues" in args[args.index("-f") + 1]: - print(json.dumps(data["blockers"])) - else: - label = next(a[6:] for a in args if a.startswith("label=")) - print(json.dumps(data.get("matches", {}).get(label, []))) -elif args[:2] == ["issue", "view"]: - if data.get("invalid_issue"): - print("null") - sys.exit(0) - print(json.dumps({"labels": [{"name": n} for n in data.get("labels", [])]})) -elif args[:2] == ["issue", "create"]: - print("https://example.invalid/issues/42") -elif args[:2] in (["issue", "edit"], ["issue", "comment"]): - if data.get("write_failure"): - sys.exit("Mock write failure") -else: - sys.exit("Unexpected mock gh call: " + repr(args)) -""" - - -class LabelMigrationTests(unittest.TestCase): - def run_script(self, script, data, *args): - with tempfile.TemporaryDirectory() as directory: - directory = Path(directory) - gh = directory / "gh" - gh.write_text(MOCK_GH) - gh.chmod(0o755) - calls = directory / "calls" - env = dict( - os.environ, - PATH=f"{directory}{os.pathsep}{os.environ['PATH']}", - GITHUB_REPOSITORY="test/repo", - MOCK_DATA=json.dumps(data), - MOCK_CALLS=str(calls), - GITHUB_OUTPUT=str(directory / "output"), - ) - result = subprocess.run( - ["bash", "-c", script, "test", *args], - cwd=ROOT, env=env, text=True, capture_output=True, - ) - recorded = [json.loads(line) for line in calls.read_text().splitlines()] if calls.exists() else [] - return result, recorded - - def workflow_script(self, name): - source = (ROOT / f".github/workflows/{name}.yml").read_text() - script = re.search(r"run: \|\n(.*?)(?=\n\s+env:)", source, re.S).group(1) - return textwrap.dedent(script).replace("${{ github.event.issue.number }}", "42") - - def category(self, labels): - return subprocess.run( - ["jq", "-e", "--slurpfile", "aliases", "dev/label-aliases.json", - "-f", "dev/check-pr-labels.jq"], - input=json.dumps({"labels": [{"name": n} for n in labels]}), - cwd=ROOT, text=True, capture_output=True, - ).returncode - - def test_category_aliases_count_once(self): - for new in ("C-bug", "C-deps", "C-enhancement", "C-refactor"): - old = ALIASES[new][0] - for labels in ([old], [new], [new, old], [new, old, "A-docs"]): - with self.subTest(labels=labels): - self.assertEqual(self.category(labels), 0) - - def test_documentation_needs_category_with_legacy_grace(self): - for labels in (["kind/documentation"], ["A-docs", "C-bug"], - ["kind/documentation", "C-enhancement"]): - self.assertEqual(self.category(labels), 0) - for labels in ([], ["A-docs"], ["C-bug", "C-deps"], - ["kind/documentation", "C-bug", "C-refactor"]): - self.assertNotEqual(self.category(labels), 0) - - def test_multiple_statuses_are_preserved(self): - for labels in (["S-needs-review", "S-nominated"], - ["lifecycle/confirmed", "S-blocked"], - ["lifecycle/confirmed", "lifecycle/blocked"]): - result, calls = self.run_script( - 'bash dev/ensure-issue-status.sh 42', {"labels": labels}) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(len(calls), 1) - - def test_status_default_prefers_new_and_falls_back(self): - for available, expected in ( - (["S-needs-triage", "lifecycle/needs-review"], "S-needs-triage"), - (["lifecycle/needs-review"], "lifecycle/needs-review"), - ): - result, calls = self.run_script( - 'bash dev/ensure-issue-status.sh 42', {"available": available}) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(calls[-1][-2:], ["--add-label", expected]) - - def test_missing_status_fails(self): - result, calls = self.run_script( - 'bash dev/ensure-issue-status.sh 42', {"available": []}) - self.assertNotEqual(result.returncode, 0) - self.assertFalse(any(call[:2] == ["issue", "edit"] for call in calls)) - - def test_release_blockers_either_name_and_both(self): - issue = {"number": 42, "title": "Failure", "url": "https://example.invalid/42"} - for old, new in ((0, 0), (1, 0), (0, 1), (1, 1), (0, 101)): - data = {"blockers": { - "legacy": {"totalCount": old, "nodes": [issue] if old else []}, - "structured": {"totalCount": new, "nodes": [issue] if new else []}, - }} - result, calls = self.run_script('bash dev/check-release-blockers.sh test/repo', data) - self.assertEqual(result.returncode, 1 if old or new else 0, result.stderr) - query = calls[0][calls[0].index("-f") + 1] - self.assertIn('labels: ["release-blocker"]', query) - self.assertIn('labels: ["P-release-blocker"]', query) - if old or new: - self.assertEqual(result.stdout.count("#42:"), 1) - - def test_blocker_cleanup_removes_both(self): - for labels in ([], ["P-release-blocker"], ["release-blocker"], - ["P-release-blocker", "release-blocker"]): - result, calls = self.run_script( - self.workflow_script("ReleaseBlockerLabelCleanUp"), {"labels": labels}) - self.assertEqual(result.returncode, 0, result.stderr) - edits = [call[-1] for call in calls if call[:2] == ["issue", "edit"]] - self.assertCountEqual(edits, labels) - - def notifier(self, data, labels="A-perf,A-testing,S-needs-triage,P-release-blocker"): - return self.run_script( - 'bash dev/notify-ci-failure.sh "$@"', data, f"--labels={labels}") - - def test_notifier_existing_aliases_remain_discoverable(self): - issue = {"number": 42, "url": "https://example.invalid/42"} - requested = ["A-perf", "A-testing", "S-needs-triage", "P-release-blocker"] - for available in (requested, [ALIASES[n][0] for n in requested], - requested + [ALIASES[n][0] for n in requested]): - for key in ["A-perf"] + ALIASES["A-perf"]: - result, calls = self.notifier({ - "available": available, "matches": {key: [issue]}}) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertFalse(any(call[:2] == ["issue", "create"] for call in calls)) - edit = next(call for call in calls if call[:2] == ["issue", "edit"]) - expected = "P-release-blocker" if "P-release-blocker" in available else "release-blocker" - self.assertEqual(edit[-2:], ["--add-label", expected]) - - def test_notifier_creates_with_resolved_labels(self): - for requested in ( - ["A-testing", "C-bug", "A-fuzzing", "S-needs-triage"], - ["A-ci-periodics", "A-testing", "S-needs-triage"], - ["A-ci-periodics-aarch64", "A-ci-periodics", "A-testing", "S-needs-triage", "P-release-blocker"], - ["A-ci-periodics-nightly", "A-ci-periodics", "A-testing", "S-needs-triage", "P-release-blocker"], - ["A-ci-periodics-mshv-6.18", "A-ci-periodics", "A-testing", "S-needs-triage", "P-release-blocker"], - ["A-fuzzing", "A-testing", "S-needs-triage", "P-release-blocker"], - ): - for available in (requested, [ALIASES[n][0] for n in requested]): - result, calls = self.notifier({"available": available}, ",".join(requested)) - self.assertEqual(result.returncode, 0, result.stderr) - create = next(call for call in calls if call[:2] == ["issue", "create"]) - self.assertEqual(create[create.index("--label") + 1:create.index("--repo"):2], available) - - def test_notifier_failures_never_create_duplicate(self): - issue = {"number": 42, "url": "https://example.invalid/42"} - for data in ( - {"available": []}, - {"api_failure": True}, - {"available": ["A-perf"], "api_failure": True}, - {"available": ["A-perf"], "matches": {"area/benchmarks": [issue]}, "write_failure": True}, - {"available": ["A-perf"], "matches": {"A-perf": None}}, - ): - result, calls = self.notifier(data, "A-perf") - self.assertNotEqual(result.returncode, 0) - self.assertFalse(any(call[:2] == ["issue", "create"] for call in calls)) - - def test_read_failures_fail_release_and_status(self): - for script in ('bash dev/check-release-blockers.sh test/repo', - 'bash dev/ensure-issue-status.sh 42'): - result, _ = self.run_script(script, {"api_failure": True}) - self.assertNotEqual(result.returncode, 0) - - def test_invalid_issue_read_fails_without_edits(self): - for script in ('bash dev/ensure-issue-status.sh 42', - self.workflow_script("ReleaseBlockerLabelCleanUp")): - result, calls = self.run_script(script, {"invalid_issue": True}) - self.assertNotEqual(result.returncode, 0) - self.assertFalse(any(call[:2] == ["issue", "edit"] for call in calls)) - - def test_empty_notifier_labels_fail_without_calls(self): - result, calls = self.notifier({}, "") - self.assertNotEqual(result.returncode, 0) - self.assertEqual(calls, []) - - def test_existing_failure_status_is_not_reset(self): - result, calls = self.notifier({ - "available": ["A-perf", "S-needs-triage"], - "matches": {"area/performance": [{"number": 42, "url": "https://example.invalid/42"}]}, - }, "A-perf,S-needs-triage") - self.assertEqual(result.returncode, 0, result.stderr) - self.assertFalse(any(call[:2] == ["issue", "edit"] for call in calls)) - - def test_golden_mode_accepts_either_label(self): - workflow = (ROOT / ".github/workflows/ValidatePullRequest.yml").read_text() - pattern = re.search(r"grep -Eqx '([^']+)'", workflow).group(1) - for labels, expected in ( - ("", 1), ("A-docs", 1), ("regen-goldens", 0), - ("S-needs-goldens", 0), ("regen-goldens\nS-needs-goldens", 0), - ): - result = subprocess.run( - ["grep", "-Eqx", pattern], input=labels + "\n", text=True) - self.assertEqual(result.returncode, expected) - - def test_static_producers_and_consumers_keep_aliases(self): - dependabot = (ROOT / ".github/dependabot.yml").read_text() - self.assertEqual(dependabot.count('"C-deps"'), 3) - self.assertEqual(dependabot.count('"kind/dependencies"'), 3) - self.assertNotIn("area/guest", dependabot) - release = (ROOT / ".github/release.yml").read_text() - self.assertEqual(release.count("C-deps"), 2) - self.assertEqual(release.count("kind/dependencies"), 2) - manage = (ROOT / ".github/workflows/ready-for-review-manage.yml").read_text() - self.assertIn("label: [S-needs-review, ready-for-review]", manage) - self.assertIn("label: ${{ matrix.label }}", manage) - self.assertIn('workflows: ["Ready-for-review (collect)"]', manage) - self.assertIn("name: ready-for-review-pr-number", manage) - - -if __name__ == "__main__": - unittest.main() From 439ab8e5b7bfdfec334d5c1c83e0b6011b17ebdd Mon Sep 17 00:00:00 2001 From: Yosh Date: Tue, 10 Nov 2026 13:16:50 +0100 Subject: [PATCH 03/14] Use only structured label names Signed-off-by: Yosh Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/bug.md | 2 +- .github/dependabot.yml | 3 --- .github/release.yml | 2 -- .github/workflows/DailyArm64.yml | 1 - .github/workflows/IssueLabelChecker.yml | 10 ++------- .github/workflows/PRLabelChecker.yml | 3 +-- .github/workflows/ReleaseBlockerCheck.yml | 2 +- .../workflows/ReleaseBlockerLabelCleanUp.yml | 10 ++++----- .github/workflows/ValidatePullRequest.yml | 4 ++-- .github/workflows/WeeklyMshv618.yml | 1 - .github/workflows/ready-for-review-label.yml | 2 +- .github/workflows/ready-for-review-manage.yml | 9 +++----- Justfile | 4 ++-- dev/check-release-blockers.sh | 22 ++++++------------- dev/notify-ci-failure.sh | 19 ---------------- docs/how-to-make-releases.md | 8 +++---- docs/snapshot-versioning.md | 2 -- 17 files changed, 28 insertions(+), 76 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index 3b6fcac24d..edaabe1dd6 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -2,7 +2,7 @@ name: Bug Report about: Report a bug in Hyperlight title: '' -labels: 'C-bug, kind/bugfix' +labels: C-bug assignees: '' --- diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 631b1e77ba..6145e90a60 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,7 +8,6 @@ updates: interval: "daily" time: "03:00" labels: - - "kind/dependencies" - "C-deps" - package-ecosystem: "cargo" directory: "/" @@ -18,7 +17,6 @@ updates: interval: "daily" time: "03:00" labels: - - "kind/dependencies" - "C-deps" open-pull-requests-limit: 20 groups: @@ -42,7 +40,6 @@ updates: interval: "daily" time: "03:00" labels: - - "kind/dependencies" - "C-deps" open-pull-requests-limit: 5 groups: diff --git a/.github/release.yml b/.github/release.yml index 5ab81660ff..4eeded7e40 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -7,9 +7,7 @@ changelog: - "*" exclude: labels: - - kind/dependencies - C-deps - title: Full Changelog (dependencies) labels: - - kind/dependencies - C-deps \ No newline at end of file diff --git a/.github/workflows/DailyArm64.yml b/.github/workflows/DailyArm64.yml index 8ff17d4b3e..f0b23d5152 100644 --- a/.github/workflows/DailyArm64.yml +++ b/.github/workflows/DailyArm64.yml @@ -103,7 +103,6 @@ jobs: # File (or update) a release-blocking GitHub issue if any job fails. The first # label (A-ci-periodics-aarch64) keeps these failures on a dedicated issue. - # The notifier accepts the existing name during the label rename. notify-failure: runs-on: ubuntu-latest needs: [build-guests, miri, build-test, run-examples, fuzzing] diff --git a/.github/workflows/IssueLabelChecker.yml b/.github/workflows/IssueLabelChecker.yml index f66a2267ad..7e88927172 100644 --- a/.github/workflows/IssueLabelChecker.yml +++ b/.github/workflows/IssueLabelChecker.yml @@ -16,15 +16,9 @@ jobs: run: | set -euo pipefail HAS_STATUS="$(gh issue view ${{ github.event.issue.number }} --json labels \ - -q 'any(.labels[].name; startswith("S-") or startswith("lifecycle/"))')" + -q 'any(.labels[].name; startswith("S-"))')" if [[ "$HAS_STATUS" == "false" ]]; then - LABELS="$(gh label list --limit 1000 --json name)" - STATUS="$(jq -er ' - [.[].name] | - if index("S-needs-triage") then "S-needs-triage" - elif index("lifecycle/needs-review") then "lifecycle/needs-review" - else error("Missing triage label") end' <<<"$LABELS")" - gh issue edit ${{ github.event.issue.number }} --add-label "$STATUS" + gh issue edit ${{ github.event.issue.number }} --add-label S-needs-triage fi env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/PRLabelChecker.yml b/.github/workflows/PRLabelChecker.yml index c8ab036f25..16a0791a23 100644 --- a/.github/workflows/PRLabelChecker.yml +++ b/.github/workflows/PRLabelChecker.yml @@ -16,9 +16,8 @@ jobs: run: | set -euo pipefail PR_NUMBER=${{ github.event.pull_request.number }} - # Accept legacy categories until the labels are renamed. gh pr view "$PR_NUMBER" --json labels | jq -e ' - [.labels[].name | select(startswith("C-") or startswith("kind/"))] | + [.labels[].name | select(startswith("C-"))] | if length == 1 then true else error("PR must have exactly one category label") end' env: diff --git a/.github/workflows/ReleaseBlockerCheck.yml b/.github/workflows/ReleaseBlockerCheck.yml index e4a3b488e3..8d20bf8748 100644 --- a/.github/workflows/ReleaseBlockerCheck.yml +++ b/.github/workflows/ReleaseBlockerCheck.yml @@ -34,7 +34,7 @@ jobs: if [[ "${{ github.ref }}" == *"refs/heads/release/"* ]]; then echo "Release branch detected - checking for release blockers..." if ! ./dev/check-release-blockers.sh "$REPO"; then - echo "::error::Release blocker check failed (P-release-blocker or release-blocker)" + echo "::error::Release blocked by open issues with P-release-blocker" exit 1 fi else diff --git a/.github/workflows/ReleaseBlockerLabelCleanUp.yml b/.github/workflows/ReleaseBlockerLabelCleanUp.yml index 7a9b3a6292..99994771ed 100644 --- a/.github/workflows/ReleaseBlockerLabelCleanUp.yml +++ b/.github/workflows/ReleaseBlockerLabelCleanUp.yml @@ -14,16 +14,14 @@ jobs: steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Remove release blocker labels from closed issue + - name: Remove P-release-blocker label from closed issue run: | set -euo pipefail ISSUE_NUMBER=${{ github.event.issue.number }} LABELS="$(gh issue view "$ISSUE_NUMBER" --json labels)" LABEL_NAMES="$(jq -e '[.labels[].name]' <<<"$LABELS")" - for label in P-release-blocker release-blocker; do - if jq -e --arg label "$label" 'index($label) != null' <<<"$LABEL_NAMES" >/dev/null; then - gh issue edit "$ISSUE_NUMBER" --remove-label "$label" - fi - done + if jq -e 'index("P-release-blocker") != null' <<<"$LABEL_NAMES" >/dev/null; then + gh issue edit "$ISSUE_NUMBER" --remove-label P-release-blocker + fi env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ValidatePullRequest.yml b/.github/workflows/ValidatePullRequest.yml index 7aff52c993..094b310eb4 100644 --- a/.github/workflows/ValidatePullRequest.yml +++ b/.github/workflows/ValidatePullRequest.yml @@ -98,7 +98,7 @@ jobs: with: docs_only: ${{ needs.docs-pr.outputs.docs-only }} - # S-needs-goldens and its rollout alias select regenerate mode. + # S-needs-goldens selects regenerate mode. # Also validate the label against the goldens state so a stale or missing label fails fast. check-golden-label: runs-on: ubuntu-latest @@ -135,7 +135,7 @@ jobs: run: | labels="$(gh pr view ${{ github.event.pull_request.number }} \ --repo ${{ github.repository }} --json labels -q '.labels[].name')" - if grep -Eqx 'S-needs-goldens|regen-goldens' <<<"$labels"; then + if grep -qx S-needs-goldens <<<"$labels"; then echo "should_regen_goldens=true" >> "$GITHUB_OUTPUT" else echo "should_regen_goldens=false" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/WeeklyMshv618.yml b/.github/workflows/WeeklyMshv618.yml index d0ac26f3a8..9cb417b8f7 100644 --- a/.github/workflows/WeeklyMshv618.yml +++ b/.github/workflows/WeeklyMshv618.yml @@ -64,7 +64,6 @@ jobs: config: ${{ matrix.config }} # The first label keeps these failures on a dedicated release-blocking issue. - # The notifier accepts an existing rollout alias. notify-failure: runs-on: ubuntu-latest needs: [build-guests, build-test, run-examples] diff --git a/.github/workflows/ready-for-review-label.yml b/.github/workflows/ready-for-review-label.yml index bb3b21d392..154abf145f 100644 --- a/.github/workflows/ready-for-review-label.yml +++ b/.github/workflows/ready-for-review-label.yml @@ -1,6 +1,6 @@ name: Ready-for-review (collect) -# Stage 1 of a two-stage pipeline that removes S-needs-review or ready-for-review from +# Stage 1 of a two-stage pipeline that removes S-needs-review from # a pull request once it is no longer awaiting review. # # Pull-request and review events originating from FORKED repositories only ever diff --git a/.github/workflows/ready-for-review-manage.yml b/.github/workflows/ready-for-review-manage.yml index 15c63b85f2..428d2950d5 100644 --- a/.github/workflows/ready-for-review-manage.yml +++ b/.github/workflows/ready-for-review-manage.yml @@ -1,6 +1,6 @@ name: Ready-for-review (manage) -# Stage 2 of the S-needs-review label pipeline, including its rollout alias. It is triggered +# Stage 2 of the S-needs-review label pipeline. It is triggered # when "Ready-for-review (collect)" completes and runs in the context of the # base repository's default branch, so it receives a read-write GITHUB_TOKEN # even when the originating event came from a fork. This is what lets us remove @@ -13,7 +13,7 @@ name: Ready-for-review (manage) # * strictly validate the pull-request number (digits only) before use. # The downstream reusable workflow only ever *removes* the label, and only when # the PR genuinely meets a removal condition, so the worst a spoofed-but-valid -# number could achieve is removing manually re-addable review labels from a PR +# number could achieve is removing a single, manually re-addable label from a PR # that already qualified for removal. on: workflow_run: @@ -66,12 +66,9 @@ jobs: manage-label: needs: read-pr - strategy: - matrix: - label: [S-needs-review, ready-for-review] # Shared workflow in the org-wide `.github` repository, pinned to a commit SHA. uses: hyperlight-dev/.github/.github/workflows/manage-ready-for-review.yml@e5e471d927d3a72676ab48433da8a99d15a32ad7 with: - label: ${{ matrix.label }} + label: S-needs-review # fromJSON turns the validated digit-string into a numeric input. pr-number: ${{ fromJSON(needs.read-pr.outputs.pr_number) }} diff --git a/Justfile b/Justfile index 20ac4b9402..528fc4df2e 100644 --- a/Justfile +++ b/Justfile @@ -788,11 +788,11 @@ snapshot-goldens-check-label has_label base_ref image=default-snapshot-goldens-i base_version="" git cat-file -e "{{ base_ref }}:${version_file}" 2>/dev/null && base_version=$(git show "{{ base_ref }}:${version_file}" | parse_version) if [[ "${version}" == "${base_version}" ]]; then - echo "::error::S-needs-goldens (or regen-goldens) is set but GOLDENS_VERSION is unchanged from {{ base_ref }} (${version}). A regen accompanies a version bump. Remove the label or bump the version." >&2 + echo "::error::S-needs-goldens is set but GOLDENS_VERSION is unchanged from {{ base_ref }} (${version}). A regen accompanies a version bump. Remove the label or bump the version." >&2 exit 1 fi if published "${version}"; then - echo "::error::S-needs-goldens (or regen-goldens) is set but goldens for ${version} are already published. Remove the label so CI verifies against the published goldens." >&2 + echo "::error::S-needs-goldens is set but goldens for ${version} are already published. Remove the label so CI verifies against the published goldens." >&2 exit 1 fi echo "snapshot-goldens-check-label: OK. ${version} bumped from ${base_version} and not yet published." diff --git a/dev/check-release-blockers.sh b/dev/check-release-blockers.sh index b7f31a5d9f..c4067706ef 100755 --- a/dev/check-release-blockers.sh +++ b/dev/check-release-blockers.sh @@ -5,7 +5,7 @@ set -o pipefail ## DESCRIPTION: ## -## This script checks for open issues with P-release-blocker or release-blocker +## This script checks for open issues with the P-release-blocker label ## in a given GitHub repository. It exits with code 1 if any blocking ## issues are found, or 0 if none are found. ## @@ -28,7 +28,7 @@ if [ -z "${1:-}" ]; then fi REPO="$1" -echo "Checking for open issues with P-release-blocker or release-blocker in $REPO..." +echo "Checking for open issues with P-release-blocker in $REPO..." # Extract owner and repo name from the argument OWNER=$(echo "$REPO" | cut -d'/' -f1) @@ -37,15 +37,7 @@ REPO_NAME=$(echo "$REPO" | cut -d'/' -f2) BLOCKING_ISSUES=$(gh api graphql -f query=' query($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { - legacy: issues(first: 100, states: OPEN, labels: ["release-blocker"]) { - totalCount - nodes { - number - title - url - } - } - structured: issues(first: 100, states: OPEN, labels: ["P-release-blocker"]) { + issues(first: 100, states: OPEN, labels: ["P-release-blocker"]) { totalCount nodes { number @@ -54,15 +46,15 @@ BLOCKING_ISSUES=$(gh api graphql -f query=' } } } - }' -f owner="$OWNER" -f repo="$REPO_NAME" --jq '.data.repository') + }' -f owner="$OWNER" -f repo="$REPO_NAME" --jq '.data.repository.issues') -BLOCKER_COUNT=$(echo "$BLOCKING_ISSUES" | jq -e '.legacy.totalCount + .structured.totalCount') +BLOCKER_COUNT=$(echo "$BLOCKING_ISSUES" | jq -e '.totalCount') if [ "$BLOCKER_COUNT" -gt 0 ]; then echo "❌ Found $BLOCKER_COUNT open release-blocking issue(s):" - echo "$BLOCKING_ISSUES" | jq -r '.legacy.nodes[], .structured.nodes[] | " - #\(.number): \(.title) (\(.url))"' + echo "$BLOCKING_ISSUES" | jq -r '.nodes[] | " - #\(.number): \(.title) (\(.url))"' echo "" - echo "Release blocked by P-release-blocker or release-blocker issues" + echo "Release blocked by open issues with P-release-blocker" exit 1 else echo "✅ No open release blocking issues found" diff --git a/dev/notify-ci-failure.sh b/dev/notify-ci-failure.sh index 7c315b25ea..9d539b6c02 100755 --- a/dev/notify-ci-failure.sh +++ b/dev/notify-ci-failure.sh @@ -75,25 +75,6 @@ fi OWNER=$(echo "$REPO" | cut -d'/' -f1) REPO_NAME=$(echo "$REPO" | cut -d'/' -f2) -# Resolve names before and after the in-place label rename. -AVAILABLE_LABELS="$(gh api "repos/$REPO/labels" --paginate --slurp | jq -e '[.[][] | .name]')" -for i in "${!LABEL_ARRAY[@]}"; do - label="${LABEL_ARRAY[$i]}" - case "$label" in - A-perf) legacy="area/performance" ;; - A-*) legacy="area/${label#A-}" ;; - C-bug) legacy="kind/bugfix" ;; - S-needs-triage) legacy="lifecycle/needs-review" ;; - P-release-blocker) legacy="release-blocker" ;; - *) legacy="$label" ;; - esac - LABEL_ARRAY[$i]="$(jq -er --arg label "$label" --arg legacy "$legacy" ' - if index($label) then $label - elif index($legacy) then $legacy - else error("Missing label: " + $label) end' <<<"$AVAILABLE_LABELS")" -done -SEARCH_LABEL="${LABEL_ARRAY[0]}" - echo "Checking for existing issues in $REPO with label '$SEARCH_LABEL'..." EXISTING_ISSUES=$(gh api graphql -f query=' query($owner: String!, $repo: String!, $label: String!) { diff --git a/docs/how-to-make-releases.md b/docs/how-to-make-releases.md index 8f033aff92..7e52a553d9 100644 --- a/docs/how-to-make-releases.md +++ b/docs/how-to-make-releases.md @@ -4,11 +4,11 @@ This document details the process of releasing a new version of Hyperlight to [c ## Resolve any release blockers -Before releasing, resolve all open issues labeled `P-release-blocker` or its -rollout alias `release-blocker`. The [issue tracker](https://github.com/hyperlight-dev/hyperlight/issues?q=is%3Aissue+is%3Aopen+label%3AP-release-blocker%2Crelease-blocker) -search includes both names. The ["Create a Release"](https://github.com/hyperlight-dev/hyperlight/actions/workflows/CreateRelease.yml) +Before releasing, resolve all open issues labeled `P-release-blocker` in the +[issue tracker](https://github.com/hyperlight-dev/hyperlight/issues?q=is%3Aissue+is%3Aopen+label%3AP-release-blocker). +The ["Create a Release"](https://github.com/hyperlight-dev/hyperlight/actions/workflows/CreateRelease.yml) workflow runs a [release blocker check](https://github.com/hyperlight-dev/hyperlight/actions/workflows/ReleaseBlockerCheck.yml) -first. It blocks release-branch publishes if either label appears on an open +first. It blocks release-branch publishes if this label appears on an open issue. Releases from other branches skip this check. ## Update Cargo.toml Versions diff --git a/docs/snapshot-versioning.md b/docs/snapshot-versioning.md index b1af1092ad..360b557f81 100644 --- a/docs/snapshot-versioning.md +++ b/docs/snapshot-versioning.md @@ -255,8 +255,6 @@ GHCR does not carry yet, so the default pull-and-verify path has nothing to load. The `S-needs-goldens` label switches the verify job into regenerate mode for that pull request. -CI also accepts `regen-goldens` during the label rename. - * **Without the label**, the job pulls the published tag set for the current `GOLDENS_VERSION` and verifies it against the branch. Missing tags fail the job. This is what turns an accidental format break into From b0ca259992844a7852c7ce01afc2068f393aaf90 Mon Sep 17 00:00:00 2001 From: Yosh Date: Tue, 10 Nov 2026 14:02:30 +0100 Subject: [PATCH 04/14] Preserve release blocker cleanup structure Signed-off-by: Yosh Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ReleaseBlockerLabelCleanUp.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ReleaseBlockerLabelCleanUp.yml b/.github/workflows/ReleaseBlockerLabelCleanUp.yml index 99994771ed..ceea2648da 100644 --- a/.github/workflows/ReleaseBlockerLabelCleanUp.yml +++ b/.github/workflows/ReleaseBlockerLabelCleanUp.yml @@ -16,12 +16,18 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Remove P-release-blocker label from closed issue run: | - set -euo pipefail ISSUE_NUMBER=${{ github.event.issue.number }} - LABELS="$(gh issue view "$ISSUE_NUMBER" --json labels)" - LABEL_NAMES="$(jq -e '[.labels[].name]' <<<"$LABELS")" - if jq -e 'index("P-release-blocker") != null' <<<"$LABEL_NAMES" >/dev/null; then - gh issue edit "$ISSUE_NUMBER" --remove-label P-release-blocker + echo "Checking if issue #$ISSUE_NUMBER has P-release-blocker label..." + + # Check if the issue has the P-release-blocker label + HAS_LABEL=$(gh issue view "$ISSUE_NUMBER" --json labels -q '.labels[] | select(.name == "P-release-blocker") | .name') + + if [ -n "$HAS_LABEL" ]; then + echo "✅ Issue #$ISSUE_NUMBER has P-release-blocker label, removing it..." + gh issue edit "$ISSUE_NUMBER" --remove-label "P-release-blocker" + echo "✅ Successfully removed P-release-blocker label from issue #$ISSUE_NUMBER" + else + echo "ℹ️ Issue #$ISSUE_NUMBER does not have P-release-blocker label, no action needed" fi env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From e981732ffbac5addb9dc1d86c62b9fbe05f2d187 Mon Sep 17 00:00:00 2001 From: Yosh Date: Tue, 10 Nov 2026 14:03:05 +0100 Subject: [PATCH 05/14] Preserve CI failure notifier behavior Signed-off-by: Yosh Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- dev/notify-ci-failure.sh | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/dev/notify-ci-failure.sh b/dev/notify-ci-failure.sh index 9d539b6c02..804939ec87 100755 --- a/dev/notify-ci-failure.sh +++ b/dev/notify-ci-failure.sh @@ -19,11 +19,11 @@ set -o pipefail ## or that the user is logged into the gh cli with an account with those permissions ## ## Usage examples: -## ./dev/notify-ci-failure.sh -## ./dev/notify-ci-failure.sh --title="Nightly Failure" --labels="A-testing,C-bug" -## ./dev/notify-ci-failure.sh --test +## ./dev/notify-fuzzing-failure.sh +## ./dev/notify-fuzzing-failure.sh --title="Nightly Failure" --labels="A-testing,C-bug" +## ./dev/notify-fuzzing-failure.sh --test ## Run this script locally like: -## GITHUB_REPOSITORY="fork/hyperlight" GITHUB_RUN_ID=1 ./dev/notify-ci-failure.sh --title="Nightly Failure" --labels="A-testing,C-bug" +## GITHUB_REPOSITORY="fork/hyperlight" GITHUB_RUN_ID=1 ./dev/notify-fuzzing-failure.sh --title="Nightly Failure" --labels="A-testing,C-bug" REPO="${GITHUB_REPOSITORY:-hyperlight-dev/hyperlight}" WORKFLOW_RUN_URL="${GITHUB_SERVER_URL:-https://github.com}/${REPO}/actions/runs/${GITHUB_RUN_ID:-unknown}" @@ -93,18 +93,22 @@ EXISTING_ISSUES=$(gh api graphql -f query=' } } } - }' -f owner="$OWNER" -f repo="$REPO_NAME" -f label="$SEARCH_LABEL" --jq '.data.repository.issues') + }' -f owner="$OWNER" -f repo="$REPO_NAME" -f label="$SEARCH_LABEL" --jq '.data.repository.issues') || EXISTING_ISSUES="" -FUZZING_ISSUE_COUNT=$(echo "$EXISTING_ISSUES" | jq -er '.totalCount') +FUZZING_ISSUES=$(echo "$EXISTING_ISSUES" | jq '.nodes[]' 2>/dev/null || echo "") +FUZZING_ISSUE_COUNT=0 +if [ -n "$FUZZING_ISSUES" ]; then + FUZZING_ISSUE_COUNT=$(echo "$FUZZING_ISSUES" | jq -s 'length' 2>/dev/null || echo "0") +fi echo "Found $FUZZING_ISSUE_COUNT existing issue(s) matching label '$SEARCH_LABEL'" if [ "$FUZZING_ISSUE_COUNT" -gt 0 ]; then - ISSUE_NUMBER=$(echo "$EXISTING_ISSUES" | jq -r '.nodes[0].number') - ISSUE_URL=$(echo "$EXISTING_ISSUES" | jq -r '.nodes[0].url') - if [[ ! "$ISSUE_NUMBER" =~ ^[0-9]+$ ]]; then - echo "Could not parse issue number from search results" >&2 - exit 1 + ISSUE_NUMBER=$(echo "$FUZZING_ISSUES" | jq -r '.number' | head -1) + ISSUE_URL=$(echo "$FUZZING_ISSUES" | jq -r '.url' | head -1) + if [ "$ISSUE_NUMBER" = "null" ] || [ -z "$ISSUE_NUMBER" ]; then + echo "⚠️ Could not parse issue number from search results; will create a new issue" + FUZZING_ISSUE_COUNT=0 else echo "Adding comment to existing issue #$ISSUE_NUMBER" COMMENT_BODY="## Job Failed Again @@ -114,9 +118,13 @@ if [ "$FUZZING_ISSUE_COUNT" -gt 0 ]; then The scheduled job has failed again. Please check the workflow logs and artifacts for details." - gh issue comment "$ISSUE_NUMBER" --body "$COMMENT_BODY" --repo "$REPO" - echo "✅ Added comment to existing issue #$ISSUE_NUMBER: $ISSUE_URL" - exit 0 + if gh issue comment "$ISSUE_NUMBER" --body "$COMMENT_BODY" --repo "$REPO"; then + echo "✅ Added comment to existing issue #$ISSUE_NUMBER: $ISSUE_URL" + exit 0 + else + echo "❌ Failed to add comment to existing issue. Will attempt to create a new issue instead." + FUZZING_ISSUE_COUNT=0 + fi fi fi @@ -140,6 +148,7 @@ The scheduled job failed during execution. This issue was automatically created --- *This issue was automatically created by the CI failure notification system.*" + # Build label args for gh issue create LABEL_ARGS=() for lbl in "${LABEL_ARRAY[@]}"; do LABEL_ARGS+=("--label" "$lbl") From aed17029f6dd2916f3a9b4c17da001bf4cbda418 Mon Sep 17 00:00:00 2001 From: Yosh Date: Tue, 10 Nov 2026 14:04:25 +0100 Subject: [PATCH 06/14] Limit remaining label migration edits to replacements Signed-off-by: Yosh Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/DailyArm64.yml | 6 +++++- .github/workflows/PRLabelChecker.yml | 17 +++++++++++------ .github/workflows/WeeklyMshv618.yml | 4 +++- dev/check-release-blockers.sh | 9 +++++---- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/.github/workflows/DailyArm64.yml b/.github/workflows/DailyArm64.yml index f0b23d5152..14a2ab12de 100644 --- a/.github/workflows/DailyArm64.yml +++ b/.github/workflows/DailyArm64.yml @@ -102,7 +102,11 @@ jobs: timeout_minutes: 35 # 5 minutes fuzzing plus setup and build on slower arm64 runners # File (or update) a release-blocking GitHub issue if any job fails. The first - # label (A-ci-periodics-aarch64) keeps these failures on a dedicated issue. + # label (A-ci-periodics-aarch64) is the de-duplication key used by + # notify-ci-failure.sh, so this workflow maintains its own issue lineage and is + # guaranteed to carry P-release-blocker, instead of commenting on another periodic + # job's issue. NOTE: that label must exist in the repo (Issues -> Labels) or the + # `gh issue create` call will fail. notify-failure: runs-on: ubuntu-latest needs: [build-guests, miri, build-test, run-examples, fuzzing] diff --git a/.github/workflows/PRLabelChecker.yml b/.github/workflows/PRLabelChecker.yml index 16a0791a23..51e0d8452f 100644 --- a/.github/workflows/PRLabelChecker.yml +++ b/.github/workflows/PRLabelChecker.yml @@ -12,13 +12,18 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Ensure exactly one category is applied + - name: Ensure exactly one "C-*" label is applied run: | - set -euo pipefail + # Count the number of "C-*" labels directly from the PR labels PR_NUMBER=${{ github.event.pull_request.number }} - gh pr view "$PR_NUMBER" --json labels | jq -e ' - [.labels[].name | select(startswith("C-"))] | - if length == 1 then true - else error("PR must have exactly one category label") end' + KIND_LABEL_COUNT=$(gh pr view "$PR_NUMBER" --json labels -q '.labels.[].name' | grep -c '^C-') + + if [[ "$KIND_LABEL_COUNT" -eq 1 ]]; then + echo "✅ Exactly one 'C-*' label is applied." + exit 0 + else + echo "❌ PR must have exactly one 'C-*' label, but found $KIND_LABEL_COUNT." + exit 1 + fi env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/WeeklyMshv618.yml b/.github/workflows/WeeklyMshv618.yml index 9cb417b8f7..8cfbcbafc5 100644 --- a/.github/workflows/WeeklyMshv618.yml +++ b/.github/workflows/WeeklyMshv618.yml @@ -63,7 +63,9 @@ jobs: arch: X64 config: ${{ matrix.config }} - # The first label keeps these failures on a dedicated release-blocking issue. + # The first label is the de-duplication key used by notify-ci-failure.sh, so + # these failures share one release-blocking issue. That label must exist in + # the repo or `gh issue create` fails. notify-failure: runs-on: ubuntu-latest needs: [build-guests, build-test, run-examples] diff --git a/dev/check-release-blockers.sh b/dev/check-release-blockers.sh index c4067706ef..e25964b377 100755 --- a/dev/check-release-blockers.sh +++ b/dev/check-release-blockers.sh @@ -5,7 +5,7 @@ set -o pipefail ## DESCRIPTION: ## -## This script checks for open issues with the P-release-blocker label +## This script checks for open issues with the 'P-release-blocker' label ## in a given GitHub repository. It exits with code 1 if any blocking ## issues are found, or 0 if none are found. ## @@ -28,12 +28,13 @@ if [ -z "${1:-}" ]; then fi REPO="$1" -echo "Checking for open issues with P-release-blocker in $REPO..." +echo "Checking for open issues with 'P-release-blocker' label in $REPO..." # Extract owner and repo name from the argument OWNER=$(echo "$REPO" | cut -d'/' -f1) REPO_NAME=$(echo "$REPO" | cut -d'/' -f2) +# Get all open issues with P-release-blocker label BLOCKING_ISSUES=$(gh api graphql -f query=' query($owner: String!, $repo: String!) { repository(owner: $owner, name: $repo) { @@ -48,13 +49,13 @@ BLOCKING_ISSUES=$(gh api graphql -f query=' } }' -f owner="$OWNER" -f repo="$REPO_NAME" --jq '.data.repository.issues') -BLOCKER_COUNT=$(echo "$BLOCKING_ISSUES" | jq -e '.totalCount') +BLOCKER_COUNT=$(echo "$BLOCKING_ISSUES" | jq '.totalCount') if [ "$BLOCKER_COUNT" -gt 0 ]; then echo "❌ Found $BLOCKER_COUNT open release-blocking issue(s):" echo "$BLOCKING_ISSUES" | jq -r '.nodes[] | " - #\(.number): \(.title) (\(.url))"' echo "" - echo "Release blocked by open issues with P-release-blocker" + echo "Release blocked by open issue(s) with 'P-release-blocker' label" exit 1 else echo "✅ No open release blocking issues found" From 9f019efd220e2a5c363e1202de752a59dddeae5c Mon Sep 17 00:00:00 2001 From: Yosh Date: Tue, 10 Nov 2026 14:05:58 +0100 Subject: [PATCH 07/14] Preserve workflow comment wording during label rename Signed-off-by: Yosh Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ValidatePullRequest.yml | 2 +- .github/workflows/ready-for-review-label.yml | 2 +- .github/workflows/ready-for-review-manage.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ValidatePullRequest.yml b/.github/workflows/ValidatePullRequest.yml index 094b310eb4..235fe47793 100644 --- a/.github/workflows/ValidatePullRequest.yml +++ b/.github/workflows/ValidatePullRequest.yml @@ -98,7 +98,7 @@ jobs: with: docs_only: ${{ needs.docs-pr.outputs.docs-only }} - # S-needs-goldens selects regenerate mode. + # Pick the goldens mode. The `S-needs-goldens` label means regenerate. No label means pull. # Also validate the label against the goldens state so a stale or missing label fails fast. check-golden-label: runs-on: ubuntu-latest diff --git a/.github/workflows/ready-for-review-label.yml b/.github/workflows/ready-for-review-label.yml index 154abf145f..77d3e61950 100644 --- a/.github/workflows/ready-for-review-label.yml +++ b/.github/workflows/ready-for-review-label.yml @@ -1,6 +1,6 @@ name: Ready-for-review (collect) -# Stage 1 of a two-stage pipeline that removes S-needs-review from +# Stage 1 of a two-stage pipeline that removes the "S-needs-review" label from # a pull request once it is no longer awaiting review. # # Pull-request and review events originating from FORKED repositories only ever diff --git a/.github/workflows/ready-for-review-manage.yml b/.github/workflows/ready-for-review-manage.yml index 428d2950d5..3fe5322b5f 100644 --- a/.github/workflows/ready-for-review-manage.yml +++ b/.github/workflows/ready-for-review-manage.yml @@ -1,6 +1,6 @@ name: Ready-for-review (manage) -# Stage 2 of the S-needs-review label pipeline. It is triggered +# Stage 2 of the two-stage "S-needs-review" label pipeline. It is triggered # when "Ready-for-review (collect)" completes and runs in the context of the # base repository's default branch, so it receives a read-write GITHUB_TOKEN # even when the originating event came from a fork. This is what lets us remove From 5de254005e48eeee4f51f7ede96fe915d1cd41b9 Mon Sep 17 00:00:00 2001 From: Yosh Date: Tue, 10 Nov 2026 14:08:47 +0100 Subject: [PATCH 08/14] Preserve release guide wording during label rename Signed-off-by: Yosh Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/how-to-make-releases.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/how-to-make-releases.md b/docs/how-to-make-releases.md index 7e52a553d9..708bd72423 100644 --- a/docs/how-to-make-releases.md +++ b/docs/how-to-make-releases.md @@ -4,12 +4,7 @@ This document details the process of releasing a new version of Hyperlight to [c ## Resolve any release blockers -Before releasing, resolve all open issues labeled `P-release-blocker` in the -[issue tracker](https://github.com/hyperlight-dev/hyperlight/issues?q=is%3Aissue+is%3Aopen+label%3AP-release-blocker). -The ["Create a Release"](https://github.com/hyperlight-dev/hyperlight/actions/workflows/CreateRelease.yml) -workflow runs a [release blocker check](https://github.com/hyperlight-dev/hyperlight/actions/workflows/ReleaseBlockerCheck.yml) -first. It blocks release-branch publishes if this label appears on an open -issue. Releases from other branches skip this check. +Before starting the release process, check for open issues labeled `P-release-blocker` in the [issue tracker](https://github.com/hyperlight-dev/hyperlight/issues?q=is%3Aissue+is%3Aopen+label%3AP-release-blocker). All such issues must be resolved (closed) before proceeding — the ["Create a Release"](https://github.com/hyperlight-dev/hyperlight/actions/workflows/CreateRelease.yml) workflow runs a [release blocker check](https://github.com/hyperlight-dev/hyperlight/actions/workflows/ReleaseBlockerCheck.yml) as its first step, and all subsequent jobs depend on it, so the publish will fail if any `P-release-blocker` issues remain open. ## Update Cargo.toml Versions From 37a1391c5322977ed0218454dc02e1d71f71b014 Mon Sep 17 00:00:00 2001 From: Yosh Date: Tue, 10 Nov 2026 14:09:04 +0100 Subject: [PATCH 09/14] Preserve release guide trailing whitespace Signed-off-by: Yosh Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/how-to-make-releases.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/how-to-make-releases.md b/docs/how-to-make-releases.md index 708bd72423..4b9afe1e1d 100644 --- a/docs/how-to-make-releases.md +++ b/docs/how-to-make-releases.md @@ -77,3 +77,4 @@ When this job is done, a new [GitHub release](https://github.com/hyperlight-dev/ 5. Cherry-pick the commits from `main` that you want to include in the patch (and resolve any conflicts). You must include the commit that updated the `CHANGELOG.md` and the commit that updated the `Cargo.toml` versions. 6. Create a tag for the patch release, e.g. `v0.4.1`, similar to the steps above. Push the tag. A job will start to try to make a new release branch for you, but it will fail because the `release/v0.4.1` branch already exists. This is expected, so don't worry about it. 7. Follow the steps above to create a new GitHub release, but this time select the `release/v0.4.1` branch in the "Use workflow from" dropdown. This will create a new GitHub release for you, and publish the updated packages to crates.io. + From a6eb73a745bedfee06d2e34116e4331bbc3289e4 Mon Sep 17 00:00:00 2001 From: Yosh Date: Tue, 10 Nov 2026 14:09:45 +0100 Subject: [PATCH 10/14] Terminate issue label workflow with a newline Signed-off-by: Yosh Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/IssueLabelChecker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/IssueLabelChecker.yml b/.github/workflows/IssueLabelChecker.yml index 7e88927172..a7efec5daa 100644 --- a/.github/workflows/IssueLabelChecker.yml +++ b/.github/workflows/IssueLabelChecker.yml @@ -21,4 +21,4 @@ jobs: gh issue edit ${{ github.event.issue.number }} --add-label S-needs-triage fi env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 1610b72e6cf28c8c361762c00b25a3673a20b5a6 Mon Sep 17 00:00:00 2001 From: Yosh Date: Tue, 10 Nov 2026 14:10:02 +0100 Subject: [PATCH 11/14] Terminate release configuration with a newline Signed-off-by: Yosh Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/release.yml b/.github/release.yml index 4eeded7e40..9817f1ba5c 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -10,4 +10,4 @@ changelog: - C-deps - title: Full Changelog (dependencies) labels: - - C-deps \ No newline at end of file + - C-deps From 473a5cc286e7e73d688006e19a0f7617df24d4c7 Mon Sep 17 00:00:00 2001 From: Yosh Date: Tue, 10 Nov 2026 14:13:40 +0100 Subject: [PATCH 12/14] Document when the review label override can be removed Signed-off-by: Yosh Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ready-for-review-manage.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ready-for-review-manage.yml b/.github/workflows/ready-for-review-manage.yml index 3fe5322b5f..b46ac2b89a 100644 --- a/.github/workflows/ready-for-review-manage.yml +++ b/.github/workflows/ready-for-review-manage.yml @@ -69,6 +69,7 @@ jobs: # Shared workflow in the org-wide `.github` repository, pinned to a commit SHA. uses: hyperlight-dev/.github/.github/workflows/manage-ready-for-review.yml@e5e471d927d3a72676ab48433da8a99d15a32ad7 with: + # TODO: Remove this override once the shared workflow defaults to S-needs-review. label: S-needs-review # fromJSON turns the validated digit-string into a numeric input. pr-number: ${{ fromJSON(needs.read-pr.outputs.pr_number) }} From 50300ffe78b6df570e85ed66897eb197199168b7 Mon Sep 17 00:00:00 2001 From: Yosh Date: Tue, 10 Nov 2026 14:14:52 +0100 Subject: [PATCH 13/14] Explain issue status label handling Signed-off-by: Yosh Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/IssueLabelChecker.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/IssueLabelChecker.yml b/.github/workflows/IssueLabelChecker.yml index a7efec5daa..31f91a58a8 100644 --- a/.github/workflows/IssueLabelChecker.yml +++ b/.github/workflows/IssueLabelChecker.yml @@ -14,6 +14,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Check and Add label run: | + # Preserve existing statuses and add S-needs-triage only when none is present. set -euo pipefail HAS_STATUS="$(gh issue view ${{ github.event.issue.number }} --json labels \ -q 'any(.labels[].name; startswith("S-"))')" From a96eac1c6fcceef300be5a77d1af6935b5035014 Mon Sep 17 00:00:00 2001 From: Yosh Date: Tue, 10 Nov 2026 14:16:39 +0100 Subject: [PATCH 14/14] Clarify category label check name Signed-off-by: Yosh Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/PRLabelChecker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/PRLabelChecker.yml b/.github/workflows/PRLabelChecker.yml index 51e0d8452f..fb13ba732d 100644 --- a/.github/workflows/PRLabelChecker.yml +++ b/.github/workflows/PRLabelChecker.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Ensure exactly one "C-*" label is applied + - name: Ensure exactly one "C-*" (Category) label is applied run: | # Count the number of "C-*" labels directly from the PR labels PR_NUMBER=${{ github.event.pull_request.number }}