diff --git a/.github/.copilot/breadcrumbs/2026-08-20-1402-kubefleet-main-backport.md b/.github/.copilot/breadcrumbs/2026-08-20-1402-kubefleet-main-backport.md new file mode 100644 index 000000000..35da47e8a --- /dev/null +++ b/.github/.copilot/breadcrumbs/2026-08-20-1402-kubefleet-main-backport.md @@ -0,0 +1,29 @@ +# Backport: kubefleet Main into Fleet + +## Overview + +Merge the latest `kubefleet-dev/kubefleet` main branch into `Azure/fleet`, preserving incoming CNCF changes while adapting module references for the Fleet repository. + +## Plan + +1. Synchronize the branch with `Azure/fleet` main and fetch `kubefleet-dev/kubefleet` main. +2. Merge `cncf/main`, preferring incoming changes for conflicts. +3. Rewrite CNCF module imports to use `go.goms.io/fleet`. +4. Remove new CRD symbolic links from the hub-agent and member-agent charts. +5. Run `make reviewable`, resolve failures caused by the backport, and commit the merge. +6. Push the branch and open a pull request against `Azure/fleet`. + +## Success Criteria + +- [x] The latest CNCF main commits are present in the merge. +- [x] No CNCF module references remain. +- [x] No new chart CRD symbolic links remain. +- [x] `make reviewable` passes. +- [ ] The merge commit is pushed and a PR is open against `Azure/fleet`. + +## Implementation Notes + +- Retained the incoming version of `.squad/templates/skills/humanizer/SKILL.md` to resolve the merge's modify/delete conflict. +- Repointed incoming support links to `Azure/fleet`. +- Removed a duplicate generated import introduced by the merge. +- Ran `make reviewable` under WSL with `GOTOOLCHAIN=go1.26.6`; all checks passed. diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 000000000..dbad00825 --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,189 @@ +name: Backport + +# Opens a backport pull request against `release-0.Y` when a merged pull +# request carries a `cherry-pick/0.Y` label. The label can be added before or +# after the merge; adding it to an already-merged PR triggers the backport +# immediately. +# +# Policy (see also CONTRIBUTING.md, "Backporting to release branches"): +# +# * Squash merges only. The automation cherry-picks the single squash commit +# recorded as the PR's merge commit. A PR merged with a merge commit, or a +# multi-commit PR merged by rebase, is NOT backported automatically - the +# workflow leaves a comment asking for a manual backport instead. This repo +# squash-merges by convention, so this only matters for exceptions. +# +# * Conflicts are never pushed. If the cherry-pick does not apply cleanly the +# workflow aborts the pick and comments on the original PR with the exact +# commands for a manual backport. It never opens a PR containing conflict +# markers. +# +# * Backport branches are bot-owned and force-pushed. The automation owns +# `cherry-pick/0.Y/pr-` branches and force-pushes them on re-runs so the +# operation is idempotent (re-labeling retries a failed backport). Do not +# push manual work to these branches; use your own branch for manual +# backports. +# +# The cherry-pick keeps the original commit message, including the author's +# Signed-off-by line (DCO), and appends the "(cherry picked from commit ...)" +# trailer via `git cherry-pick -x`. +# +# NOTE: pull_request_target grants a write token, so this workflow must never +# check out or execute code from the PR. It only manipulates git history +# (cherry-pick of an already-merged commit) and calls the GitHub API. All +# PR-controlled strings (title, label names) are passed through environment +# variables, never interpolated into shell text. + +on: + pull_request_target: + types: [closed, labeled] + +permissions: + contents: write + pull-requests: write + +# One backport run per PR at a time: a `closed` event and a late `labeled` +# event for the same PR must not race on the same bot branch. +concurrency: + group: backport-${{ github.event.pull_request.number }} + cancel-in-progress: false + +jobs: + backport: + # Run only for merged PRs, and only when the event can introduce a + # cherry-pick label: the merge itself, or a cherry-pick/* label added + # to an already-merged PR. + if: > + github.event.pull_request.merged == true && + (github.event.action == 'closed' || + startsWith(github.event.label.name, 'cherry-pick/')) + runs-on: ubuntu-latest + steps: + - name: Checkout base repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Full history: the merge commit and the release branches must both + # be reachable for the cherry-pick. + fetch-depth: 0 + # Deliberately the default ref (base repo main), never the PR head. + persist-credentials: true + + - name: Cherry-pick to release branches + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }} + EVENT_ACTION: ${{ github.event.action }} + EVENT_LABEL: ${{ github.event.label.name || '' }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + + # Commit as the github-actions[bot] app user so backport commits are + # attributed to the automation (the original author and their DCO + # sign-off are preserved by the cherry-pick). The user ID is resolved + # from the API so the noreply address is provably correct; on API + # failure fall back to the app user's long-stable known ID. Note that + # gh prints the error body to stdout on failure, hence the numeric + # guard rather than a plain `|| echo`. + bot_id="$(gh api 'users/github-actions%5Bbot%5D' --jq .id 2>/dev/null || true)" + case "${bot_id}" in + ''|*[!0-9]*) bot_id=41898282 ;; + esac + git config user.name "github-actions[bot]" + git config user.email "${bot_id}+github-actions[bot]@users.noreply.github.com" + + # Collect the cherry-pick labels to act on: just the added label for + # a `labeled` event, every cherry-pick label on the PR for `closed`. + if [ "${EVENT_ACTION}" = "labeled" ]; then + labels="${EVENT_LABEL}" + else + labels="$(gh api "repos/${REPO}/issues/${PR_NUMBER}/labels" \ + --jq '.[].name | select(startswith("cherry-pick/"))')" + fi + if [ -z "${labels}" ]; then + echo "No cherry-pick/* labels on PR #${PR_NUMBER}; nothing to do." + exit 0 + fi + + comment() { + gh pr comment "${PR_NUMBER}" --repo "${REPO}" --body "$1" + } + + # Backport only squash commits (single parent) whose subject carries + # this PR's number - the shape every squash-merged PR in this repo + # has. This rejects merge commits outright and refuses to guess on + # rebase-merged multi-commit PRs, where picking only the merge SHA + # would silently drop the earlier commits. + parent_count="$(git rev-list --parents -n 1 "${MERGE_SHA}" | wc -w)" + subject="$(git log --format=%s -n 1 "${MERGE_SHA}")" + if [ "${parent_count}" -ne 2 ] || ! grep -q "(#${PR_NUMBER})" <<<"${subject}"; then + comment ":no_entry: Automatic backport skipped: PR #${PR_NUMBER} was not squash-merged (or its merge commit does not reference the PR), so the merge commit cannot be cherry-picked safely. Please backport manually." + exit 0 + fi + + mapfile -t label_list <<<"${labels}" + failed="" + for label in "${label_list[@]}"; do + [ -n "${label}" ] || continue + minor="${label#cherry-pick/}" + target="release-${minor}" + bot_branch="cherry-pick/${minor}/pr-${PR_NUMBER}" + + # Soft-fail (comment, but keep the run green): labels may + # legitimately be applied before the release branch is cut; the + # backport is picked up by re-adding the label once it exists. + if ! git rev-parse --verify --quiet "origin/${target}" >/dev/null; then + comment ":no_entry: Backport to \`${target}\` skipped: the branch does not exist. If the \`${label}\` label is correct, create the release branch first and re-add the label to retry." + continue + fi + + echo "Backporting ${MERGE_SHA} to ${target} (label: ${label})" + git switch --force-create "${bot_branch}" "origin/${target}" + + if ! git cherry-pick -x "${MERGE_SHA}"; then + git cherry-pick --abort || true + comment ":warning: Backport to \`${target}\` failed: the cherry-pick has conflicts. Please backport manually: + + \`\`\` + git fetch origin + git switch -c backport-${PR_NUMBER}-to-${target} origin/${target} + git cherry-pick -x ${MERGE_SHA} + # resolve conflicts, then + git cherry-pick --continue + git push origin backport-${PR_NUMBER}-to-${target} + \`\`\` + + Re-adding the \`${label}\` label retries the automatic backport." + failed="true" + continue + fi + + # Bot-owned branch: force-push so retries are idempotent. + git push --force origin "${bot_branch}" + + # Reuse the open backport PR for this branch if one exists. + existing="$(gh pr list --repo "${REPO}" --head "${bot_branch}" \ + --base "${target}" --state open --json number --jq '.[0].number // empty')" + if [ -n "${existing}" ]; then + echo "Backport PR #${existing} already open for ${bot_branch}; branch updated." + continue + fi + + # Suffix the target rather than prefixing it: PR-title lint runs on + # backport PRs too and requires the conventional prefix (feat:, + # fix:, ...) at the start of the title. + title="$(gh pr view "${PR_NUMBER}" --repo "${REPO}" --json title --jq .title)" + url="$(gh pr create --repo "${REPO}" \ + --base "${target}" --head "${bot_branch}" \ + --title "${title} [backport ${target}]" \ + --body "Automated cherry-pick of #${PR_NUMBER} to \`${target}\`, requested via the \`${label}\` label. + + > [!NOTE] + > Workflows do not run automatically on PRs opened by github-actions; a maintainer may need to close and reopen this PR (or push an empty commit) to trigger CI.")" + comment ":cherries: Backport to \`${target}\` opened: ${url}" + done + + if [ -n "${failed}" ]; then + exit 1 + fi diff --git a/.github/workflows/chart.yml b/.github/workflows/chart.yml index 57c7965c5..600df71d6 100644 --- a/.github/workflows/chart.yml +++ b/.github/workflows/chart.yml @@ -39,7 +39,7 @@ jobs: group: helm-chart-publish-gh-pages cancel-in-progress: false steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: true fetch-depth: 0 @@ -56,10 +56,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Login to GitHub Container Registry - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 497406bc2..4d3d8d6b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ on: paths-ignore: [docs/**, "**.md", "**.mdx", "**.png", "**.jpg"] env: - GO_VERSION: '1.25.12' + GO_VERSION: '1.26.6' CERT_MANAGER_VERSION: 'v1.16.2' jobs: @@ -24,7 +24,7 @@ jobs: steps: - name: Detect No-op Changes id: noop - uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5.3.1 + uses: fkirc/skip-duplicate-actions@b974a9395958c231af965b70070979a577efa578 # v5.3.2 with: github_token: ${{ secrets.GITHUB_TOKEN }} do_not_skip: '["workflow_dispatch", "schedule", "push"]' @@ -36,12 +36,12 @@ jobs: if: needs.detect-noop.outputs.noop != 'true' steps: - name: Set up Go - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 with: go-version: ${{ env.GO_VERSION }} - name: Check out code into the Go module directory - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Ginkgo CLI run: | @@ -76,7 +76,7 @@ jobs: KUBEFLEET_CI_TEST_RUNNER_NAME: 'ginkgo' - name: Upload Codecov report - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: ## Repository upload token - get it from codecov.io. Required only for private repositories token: ${{ secrets.CODECOV_TOKEN }} @@ -111,12 +111,12 @@ jobs: if: needs.detect-noop.outputs.noop != 'true' steps: - name: Set up Go - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 with: go-version: ${{ env.GO_VERSION }} - name: Check out code into the Go module directory - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Move Docker data directory to /mnt # The default storage device on GitHub-hosted runners is running low during e2e tests. diff --git a/.github/workflows/code-lint.yml b/.github/workflows/code-lint.yml index 296a29060..d96ee1cf1 100644 --- a/.github/workflows/code-lint.yml +++ b/.github/workflows/code-lint.yml @@ -14,7 +14,7 @@ on: env: # Common versions - GO_VERSION: "1.25.12" + GO_VERSION: "1.26.6" jobs: detect-noop: @@ -24,7 +24,7 @@ jobs: steps: - name: Detect No-op Changes id: noop - uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5.3.1 + uses: fkirc/skip-duplicate-actions@b974a9395958c231af965b70070979a577efa578 # v5.3.2 with: github_token: ${{ secrets.GITHUB_TOKEN }} do_not_skip: '["workflow_dispatch", "schedule", "push"]' @@ -37,12 +37,12 @@ jobs: steps: - name: Setup Go - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 with: go-version: ${{ env.GO_VERSION }} - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: true @@ -58,12 +58,12 @@ jobs: steps: - name: Set up Go ${{ env.GO_VERSION }} - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 with: go-version: ${{ env.GO_VERSION }} - name: Check out code into the Go module directory - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: golangci-lint run: make lint @@ -76,7 +76,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Helm uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index dc1667315..edaa4ff37 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,11 +38,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4 + uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -56,7 +56,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4 + uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 # ℹ️ Command-line programs to run using the OS shell. # πŸ“š See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -69,4 +69,4 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4 + uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index dfb9a51b0..27b6f5379 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -16,7 +16,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4.1.7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # master with: check_filenames: true diff --git a/.github/workflows/markdown-lint.yml b/.github/workflows/markdown-lint.yml index 9cfb00bb2..060e9ea87 100644 --- a/.github/workflows/markdown-lint.yml +++ b/.github/workflows/markdown-lint.yml @@ -10,8 +10,8 @@ jobs: markdown-link-check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - uses: tcort/github-action-markdown-link-check@e7c7a18363c842693fadde5d41a3bd3573a7a225 # v1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: tcort/github-action-markdown-link-check@e047c5b37f24ab722bbef1a27b6fab7f96bc4068 # v1 with: # this will only show errors in the output use-quiet-mode: 'yes' diff --git a/.github/workflows/markdown.links.config.json b/.github/workflows/markdown.links.config.json index c4914cd6e..5f498d811 100644 --- a/.github/workflows/markdown.links.config.json +++ b/.github/workflows/markdown.links.config.json @@ -7,5 +7,13 @@ "timeout": "5s", "retryOn429": true, "retryCount": 5, - "fallbackRetryDelay": "30s" + "fallbackRetryDelay": "30s", + "ignorePatterns": [ + { + "pattern": "^mailto:" + }, + { + "pattern": "^https://cloud-native\\.slack\\.com/archives/" + } + ] } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 09b8bc79b..07762796d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ env: HUB_AGENT_IMAGE_NAME: hub-agent MEMBER_AGENT_IMAGE_NAME: member-agent REFRESH_TOKEN_IMAGE_NAME: refresh-token - GO_VERSION: "1.25.12" + GO_VERSION: "1.26.6" jobs: export-registry: @@ -44,17 +44,17 @@ jobs: runs-on: ubuntu-latest steps: - name: Set up Go ${{ env.GO_VERSION }} - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 with: go-version: ${{ env.GO_VERSION }} - name: Checkout code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ needs.export-registry.outputs.tag }} - name: Login to ghcr.io - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f with: registry: ghcr.io username: ${{ github.actor }} @@ -94,3 +94,55 @@ jobs: echo " - ${{ env.REGISTRY }}/${IMAGE}:${VERSION}" fi done + + # Publish the raw CRDs as a standalone release asset so consumers can install + # them without pulling a Helm chart. The bundle carries the unmodified CRDs the + # charts install (no downstream-specific labels), split into crds/hub and + # crds/member so each set can be applied to the right cluster. Runs after the + # images are published so a release is only created once the build succeeds. + publish-crds: + needs: [export-registry, build-and-publish] + runs-on: ubuntu-latest + permissions: + contents: write + env: + TAG: ${{ needs.export-registry.outputs.tag }} + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.export-registry.outputs.tag }} + + - name: Package CRDs + run: make crd-package TAG="${TAG}" + + - name: Create or update the release and upload the CRD bundle + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + created_draft="" + if ! gh release view "${TAG}" >/dev/null 2>&1; then + # Any semver pre-release suffix (-rc.N, -alpha, -beta, ...) is a prerelease. + prerelease="" + case "${TAG}" in *-*) prerelease="--prerelease" ;; esac + # Create as a draft first so a partially uploaded release is never public. + # --verify-tag: `gh release create` creates the tag itself when it is + # missing, pointing at the default branch's head. A tag that does not + # exist at all already fails earlier, at build-and-publish's checkout, + # so this guards the narrower case where the ref resolved to something + # that is not the tag - the release would then point at a different + # commit than the images were built from. + gh release create "${TAG}" --title "${TAG}" --generate-notes --draft --verify-tag ${prerelease} + created_draft="true" + fi + gh release upload "${TAG}" \ + "_crd-package/kubefleet-crds-${TAG}.tgz" \ + "_crd-package/kubefleet-crds-${TAG}.tgz.sha256" \ + --clobber + # Only publish releases this job created; never flip a maintainer's existing release. + if [ "${created_draft}" = "true" ]; then + gh release edit "${TAG}" --draft=false + elif [ "$(gh release view "${TAG}" --json isDraft --jq .isDraft)" = "true" ]; then + echo "::warning::Release ${TAG} already existed as a draft; the CRD bundle was uploaded but the release was left unpublished. Publish it manually." + fi diff --git a/.github/workflows/setup-release.yml b/.github/workflows/setup-release.yml index ab7aadcaf..65cd8984b 100644 --- a/.github/workflows/setup-release.yml +++ b/.github/workflows/setup-release.yml @@ -24,14 +24,30 @@ env: jobs: export: runs-on: ubuntu-latest + # Validates an input and writes step outputs; it touches no GitHub API, + # so it needs no token. Without this it inherits the caller's + # permissions, and chart.yml calls it with contents/packages write. + permissions: {} outputs: registry: ${{ steps.setup.outputs.registry }} tag: ${{ steps.setup.outputs.tag }} version: ${{ steps.setup.outputs.version }} steps: - id: setup + # The tag arrives as an environment variable rather than being + # interpolated into the script body: `TAG="${{ inputs.tag }}"` + # expands the value in the shell before the validation below can + # reject it. Both callers are affected, not just the + # workflow_dispatch input - backticks and $(...) are legal in git + # ref names and still match the v*.*.* push filter. Reaching + # either needs repository write access, so this is defence in + # depth, but the outputs below are interpolated into shell by + # every downstream job, which makes the regex the trust boundary + # for all of them. + env: + RELEASE_TAG: ${{ inputs.tag }} run: | - TAG="${{ inputs.tag }}" + TAG="${RELEASE_TAG}" if [[ ! "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then echo "Error: Invalid release tag '${TAG}'. Expected format: vMAJOR.MINOR.PATCH or vMAJOR.MINOR.PATCH-rc.N" exit 1 diff --git a/.github/workflows/squad-ci.yml b/.github/workflows/squad-ci.yml index c5e2a3981..151d194e8 100644 --- a/.github/workflows/squad-ci.yml +++ b/.github/workflows/squad-ci.yml @@ -15,7 +15,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v7.0.1 - name: Build and test run: | diff --git a/.github/workflows/squad-docs.yml b/.github/workflows/squad-docs.yml index 209349bfe..d21d4f9be 100644 --- a/.github/workflows/squad-docs.yml +++ b/.github/workflows/squad-docs.yml @@ -18,7 +18,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v7.0.1 - name: Build docs run: | diff --git a/.github/workflows/squad-heartbeat.yml b/.github/workflows/squad-heartbeat.yml index 2fb36a3bd..2a034b684 100644 --- a/.github/workflows/squad-heartbeat.yml +++ b/.github/workflows/squad-heartbeat.yml @@ -25,7 +25,7 @@ jobs: heartbeat: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v7.0.1 - name: Check triage script id: check-script @@ -48,7 +48,7 @@ jobs: - name: Ralph β€” Apply triage decisions if: steps.check-script.outputs.has_script == 'true' && hashFiles('triage-results.json') != '' - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: script: | const fs = require('fs'); @@ -100,7 +100,7 @@ jobs: # Copilot auto-assign step (uses PAT if available) - name: Ralph β€” Assign @copilot issues if: success() - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: github-token: ${{ secrets.COPILOT_ASSIGN_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/squad-insider-release.yml b/.github/workflows/squad-insider-release.yml index c46826aa8..65512a7ae 100644 --- a/.github/workflows/squad-insider-release.yml +++ b/.github/workflows/squad-insider-release.yml @@ -12,7 +12,7 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v7.0.1 with: fetch-depth: 0 diff --git a/.github/workflows/squad-issue-assign.yml b/.github/workflows/squad-issue-assign.yml index 1bec8ed25..74163707a 100644 --- a/.github/workflows/squad-issue-assign.yml +++ b/.github/workflows/squad-issue-assign.yml @@ -14,10 +14,10 @@ jobs: if: startsWith(github.event.label.name, 'squad:') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v7.0.1 - name: Identify assigned member and trigger work - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: script: | const fs = require('fs'); @@ -112,7 +112,7 @@ jobs: # Separate step: assign @copilot using PAT (required for coding agent) - name: Assign @copilot coding agent if: github.event.label.name == 'squad:copilot' - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: github-token: ${{ secrets.COPILOT_ASSIGN_TOKEN }} script: | diff --git a/.github/workflows/squad-label-enforce.yml b/.github/workflows/squad-label-enforce.yml index 10cce2682..bac19d50c 100644 --- a/.github/workflows/squad-label-enforce.yml +++ b/.github/workflows/squad-label-enforce.yml @@ -12,10 +12,10 @@ jobs: enforce: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v7.0.1 - name: Enforce mutual exclusivity - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: script: | const issue = context.payload.issue; diff --git a/.github/workflows/squad-preview.yml b/.github/workflows/squad-preview.yml index 3a2887517..6b3c7647e 100644 --- a/.github/workflows/squad-preview.yml +++ b/.github/workflows/squad-preview.yml @@ -12,7 +12,7 @@ jobs: validate: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v7.0.1 - name: Build and test run: | diff --git a/.github/workflows/squad-promote.yml b/.github/workflows/squad-promote.yml index daf829671..6be1912c0 100644 --- a/.github/workflows/squad-promote.yml +++ b/.github/workflows/squad-promote.yml @@ -18,7 +18,7 @@ jobs: name: Promote dev β†’ preview runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v7.0.1 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} @@ -70,7 +70,7 @@ jobs: needs: dev-to-preview runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v7.0.1 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/squad-release.yml b/.github/workflows/squad-release.yml index 15e6c0e67..82896f620 100644 --- a/.github/workflows/squad-release.yml +++ b/.github/workflows/squad-release.yml @@ -12,7 +12,7 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v7.0.1 with: fetch-depth: 0 diff --git a/.github/workflows/squad-triage.yml b/.github/workflows/squad-triage.yml index de92a246c..af89291f8 100644 --- a/.github/workflows/squad-triage.yml +++ b/.github/workflows/squad-triage.yml @@ -13,10 +13,10 @@ jobs: if: github.event.label.name == 'squad' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v7.0.1 - name: Triage issue via Lead agent - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: script: | const fs = require('fs'); diff --git a/.github/workflows/sync-squad-labels.yml b/.github/workflows/sync-squad-labels.yml index e6a7f6c63..1cf5bab02 100644 --- a/.github/workflows/sync-squad-labels.yml +++ b/.github/workflows/sync-squad-labels.yml @@ -14,10 +14,10 @@ jobs: sync-labels: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v7.0.1 - name: Parse roster and sync labels - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: script: | const fs = require('fs'); diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 1734fbdc0..b64652a0b 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -1,212 +1,215 @@ -name: Trivy Vulnerability Scanner -on: - schedule: - - cron: '0 6 * * *' # Daily at 6:00 AM UTC - push: - branches: - - main - # Publish semver tags as releases. - tags: - - 'v*.*.*' - workflow_dispatch: {} - -permissions: - contents: read - packages: write - issues: write - -env: - REGISTRY: ghcr.io - HUB_AGENT_IMAGE_NAME: hub-agent - MEMBER_AGENT_IMAGE_NAME: member-agent - REFRESH_TOKEN_IMAGE_NAME: refresh-token - - GO_VERSION: '1.25.12' - -jobs: - export-registry: - runs-on: ubuntu-latest - outputs: - registry: ${{ steps.export.outputs.registry }} - steps: - - id: export - run: | - # registry must be in lowercase - # store the images under dev - # TODO: need to cleanup dev images periodically - echo "registry=$(echo "${{ env.REGISTRY }}/${{ github.repository }}" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT" - scan-images: - needs: export-registry - env: - REGISTRY: ${{ needs.export-registry.outputs.registry }} - runs-on: ubuntu-latest - steps: - - name: Set up Go ${{ env.GO_VERSION }} - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 - with: - go-version: ${{ env.GO_VERSION }} - - - name: Checkout code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Login to ${{ env.REGISTRY }} - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: generate image version - run: echo "IMAGE_VERSION=$(git rev-parse --short=7 HEAD)" >> "$GITHUB_ENV" - - # Note: scheduled runs rebuild images to scan the latest code on main. - # This ensures we catch newly disclosed CVEs against the current source. - - name: Build and push images to registry with tag ${{ env.IMAGE_VERSION }} - run: | - make push - env: - REGISTRY: ${{ env.REGISTRY}} - TAG: ${{ env.IMAGE_VERSION }} - - - name: Scan ${{ env.REGISTRY }}/${{ env.HUB_AGENT_IMAGE_NAME }}:${{ env.IMAGE_VERSION }} - uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 - with: - image-ref: ${{ env.REGISTRY }}/${{ env.HUB_AGENT_IMAGE_NAME }}:${{ env.IMAGE_VERSION }} - format: 'json' - output: 'trivy-hub-agent.json' - ignore-unfixed: true - vuln-type: 'os,library' - severity: 'CRITICAL,HIGH' - timeout: '5m0s' - env: - TRIVY_USERNAME: ${{ github.actor }} - TRIVY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} - TRIVY_DB_REPOSITORY: mcr.microsoft.com/mirror/ghcr/aquasecurity/trivy-db - - - name: Scan ${{ env.REGISTRY }}/${{ env.MEMBER_AGENT_IMAGE_NAME }}:${{ env.IMAGE_VERSION }} - uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 - with: - image-ref: ${{ env.REGISTRY }}/${{ env.MEMBER_AGENT_IMAGE_NAME }}:${{ env.IMAGE_VERSION }} - format: 'json' - output: 'trivy-member-agent.json' - ignore-unfixed: true - vuln-type: 'os,library' - severity: 'CRITICAL,HIGH' - timeout: '5m0s' - env: - TRIVY_USERNAME: ${{ github.actor }} - TRIVY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} - TRIVY_DB_REPOSITORY: mcr.microsoft.com/mirror/ghcr/aquasecurity/trivy-db - - - name: Scan ${{ env.REGISTRY }}/${{ env.REFRESH_TOKEN_IMAGE_NAME }}:${{ env.IMAGE_VERSION }} - uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 - with: - image-ref: ${{ env.REGISTRY }}/${{ env.REFRESH_TOKEN_IMAGE_NAME }}:${{ env.IMAGE_VERSION }} - format: 'json' - output: 'trivy-refresh-token.json' - ignore-unfixed: true - vuln-type: 'os,library' - severity: 'CRITICAL,HIGH' - timeout: '5m0s' - env: - TRIVY_USERNAME: ${{ github.actor }} - TRIVY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} - TRIVY_DB_REPOSITORY: mcr.microsoft.com/mirror/ghcr/aquasecurity/trivy-db - - - name: Check for vulnerabilities - id: check-vulns - run: | - has_vulns=false - for file in trivy-hub-agent.json trivy-member-agent.json trivy-refresh-token.json; do - count=$(jq '[.Results[]? | .Vulnerabilities[]?] | length' "$file") - if [ "$count" -gt 0 ]; then - has_vulns=true - break - fi - done - echo "has_vulns=$has_vulns" >> "$GITHUB_OUTPUT" - - - name: Fail on vulnerabilities (non-scheduled runs) - if: steps.check-vulns.outputs.has_vulns == 'true' && github.event_name != 'schedule' - run: | - echo "::error::Vulnerabilities found. See trivy scan output." - for file in trivy-hub-agent.json trivy-member-agent.json trivy-refresh-token.json; do - echo "--- $file ---" - jq -r '.Results[]? | .Vulnerabilities[]? | "\(.VulnerabilityID) \(.Severity) \(.PkgName) \(.InstalledVersion) -> \(.FixedVersion)"' "$file" - done - exit 1 - - - name: Build vulnerability summary - if: steps.check-vulns.outputs.has_vulns == 'true' && github.event_name == 'schedule' - id: vuln-summary - run: | - { - echo 'body<@\`" - echo "2. Run \`go mod tidy\` to clean up dependencies." - echo "" - echo "**OS / base-image CVEs:**" - echo "1. Update the base image in the relevant \`Dockerfile\` under \`docker/\`." - echo "" - echo "**Then verify:**" - echo "1. Run \`make build\` to verify the build passes." - echo "2. Run \`make test\` to verify tests pass." - echo "" - echo "**Review:** Request review from \`@kubefleet-dev/kubefleet-secops\` on the resulting PR." - echo 'EOF' - } >> "$GITHUB_OUTPUT" - - - name: Create issue for Copilot - if: steps.check-vulns.outputs.has_vulns == 'true' && github.event_name == 'schedule' - uses: actions/github-script@v7 - with: - script: | - const today = new Date().toISOString().split('T')[0]; - const title = `fix: address trivy CVEs found on ${today}`; - - // Check if an open issue already exists for today - const existing = await github.rest.issues.listForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - labels: 'security,trivy', - per_page: 100 - }); - const alreadyExists = existing.data.some(i => i.title === title); - if (alreadyExists) { - console.log('Issue already exists for today, skipping.'); - return; - } - - const body = process.env.ISSUE_BODY; - const issue = await github.rest.issues.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title: title, - body: body + '\n\n/cc @kubefleet-dev/kubefleet-secops', - labels: ['security', 'trivy'], - assignees: ['copilot'], - }); - console.log(`Created issue #${issue.data.number}`); - env: - ISSUE_BODY: ${{ steps.vuln-summary.outputs.body }} - +name: Trivy Vulnerability Scanner +on: + schedule: + - cron: '0 6 * * *' # Daily at 6:00 AM UTC + push: + branches: + - main + # Publish semver tags as releases. + tags: + - 'v*.*.*' + workflow_dispatch: {} + +permissions: + contents: read + packages: write + issues: write + +env: + REGISTRY: ghcr.io + HUB_AGENT_IMAGE_NAME: hub-agent + MEMBER_AGENT_IMAGE_NAME: member-agent + REFRESH_TOKEN_IMAGE_NAME: refresh-token + + GO_VERSION: '1.26.6' + +jobs: + export-registry: + runs-on: ubuntu-latest + outputs: + registry: ${{ steps.export.outputs.registry }} + steps: + - id: export + run: | + # registry must be in lowercase + # store the images under dev + # TODO: need to cleanup dev images periodically + echo "registry=$(echo "${{ env.REGISTRY }}/${{ github.repository }}" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT" + scan-images: + needs: export-registry + env: + REGISTRY: ${{ needs.export-registry.outputs.registry }} + runs-on: ubuntu-latest + steps: + - name: Set up Go ${{ env.GO_VERSION }} + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Login to ${{ env.REGISTRY }} + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: generate image version + run: echo "IMAGE_VERSION=$(git rev-parse --short=7 HEAD)" >> "$GITHUB_ENV" + + # Note: scheduled runs rebuild images to scan the latest code on main. + # This ensures we catch newly disclosed CVEs against the current source. + - name: Build and push images to registry with tag ${{ env.IMAGE_VERSION }} + run: | + make push + env: + REGISTRY: ${{ env.REGISTRY}} + TAG: ${{ env.IMAGE_VERSION }} + + - name: Scan ${{ env.REGISTRY }}/${{ env.HUB_AGENT_IMAGE_NAME }}:${{ env.IMAGE_VERSION }} + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + image-ref: ${{ env.REGISTRY }}/${{ env.HUB_AGENT_IMAGE_NAME }}:${{ env.IMAGE_VERSION }} + format: 'json' + output: 'trivy-hub-agent.json' + ignore-unfixed: true + vuln-type: 'os,library' + severity: 'CRITICAL,HIGH' + timeout: '5m0s' + env: + TRIVY_USERNAME: ${{ github.actor }} + TRIVY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + TRIVY_DB_REPOSITORY: mcr.microsoft.com/oss/v2/aquasecurity/trivy-db + + - name: Scan ${{ env.REGISTRY }}/${{ env.MEMBER_AGENT_IMAGE_NAME }}:${{ env.IMAGE_VERSION }} + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + image-ref: ${{ env.REGISTRY }}/${{ env.MEMBER_AGENT_IMAGE_NAME }}:${{ env.IMAGE_VERSION }} + format: 'json' + output: 'trivy-member-agent.json' + ignore-unfixed: true + vuln-type: 'os,library' + severity: 'CRITICAL,HIGH' + timeout: '5m0s' + env: + TRIVY_USERNAME: ${{ github.actor }} + TRIVY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + TRIVY_DB_REPOSITORY: mcr.microsoft.com/oss/v2/aquasecurity/trivy-db + + - name: Scan ${{ env.REGISTRY }}/${{ env.REFRESH_TOKEN_IMAGE_NAME }}:${{ env.IMAGE_VERSION }} + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + image-ref: ${{ env.REGISTRY }}/${{ env.REFRESH_TOKEN_IMAGE_NAME }}:${{ env.IMAGE_VERSION }} + format: 'json' + output: 'trivy-refresh-token.json' + ignore-unfixed: true + vuln-type: 'os,library' + severity: 'CRITICAL,HIGH' + timeout: '5m0s' + env: + TRIVY_USERNAME: ${{ github.actor }} + TRIVY_PASSWORD: ${{ secrets.GITHUB_TOKEN }} + TRIVY_DB_REPOSITORY: mcr.microsoft.com/oss/v2/aquasecurity/trivy-db + + - name: Check for vulnerabilities + id: check-vulns + run: | + has_vulns=false + for file in trivy-hub-agent.json trivy-member-agent.json trivy-refresh-token.json; do + count=$(jq '[.Results[]? | .Vulnerabilities[]?] | length' "$file") + if [ "$count" -gt 0 ]; then + has_vulns=true + break + fi + done + echo "has_vulns=$has_vulns" >> "$GITHUB_OUTPUT" + + - name: Fail on vulnerabilities (non-scheduled runs) + if: steps.check-vulns.outputs.has_vulns == 'true' && github.event_name != 'schedule' + run: | + echo "::error::Vulnerabilities found. See trivy scan output." + for file in trivy-hub-agent.json trivy-member-agent.json trivy-refresh-token.json; do + echo "--- $file ---" + jq -r '.Results[]? | .Vulnerabilities[]? | "\(.VulnerabilityID) \(.Severity) \(.PkgName) \(.InstalledVersion) -> \(.FixedVersion)"' "$file" + done + exit 1 + + - name: Build vulnerability summary + if: steps.check-vulns.outputs.has_vulns == 'true' && github.event_name == 'schedule' + id: vuln-summary + run: | + { + echo 'body<@\`" + echo "2. Run \`go mod tidy\` to clean up dependencies." + echo "" + echo "**OS / base-image CVEs:**" + echo "1. Update the base image in the relevant \`Dockerfile\` under \`docker/\`." + echo "" + echo "**Then verify:**" + echo "1. Run \`make build\` to verify the build passes." + echo "2. Run \`make test\` to verify tests pass." + echo "" + echo "**Review:** Request review from \`@kubefleet-dev/kubefleet-secops\` on the resulting PR." + echo 'EOF' + } >> "$GITHUB_OUTPUT" + + - name: Create or update security issue + if: steps.check-vulns.outputs.has_vulns == 'true' && github.event_name == 'schedule' + uses: actions/github-script@v9 + with: + script: | + const today = new Date().toISOString().split('T')[0]; + const title = `fix: address trivy CVEs found on ${today}`; + const securityTeam = '@kubefleet-dev/kubefleet-secops'; + const body = `${process.env.ISSUE_BODY}\n\n### Security owners\n${securityTeam}`; + + // Check if an open issue already exists for today + const existing = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: 'security,trivy', + per_page: 100 + }); + const issue = existing.data.find(i => i.title === title); + if (issue) { + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: body + }); + console.log('Updated the existing issue with the current scan and security team mention.'); + } else { + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: title, + body: body, + labels: ['security', 'trivy'] + }); + } + env: + ISSUE_BODY: ${{ steps.vuln-summary.outputs.body }} diff --git a/.github/workflows/upgrade.yml b/.github/workflows/upgrade.yml index f15337add..0ba9e395b 100644 --- a/.github/workflows/upgrade.yml +++ b/.github/workflows/upgrade.yml @@ -17,7 +17,7 @@ on: paths-ignore: [docs/**, "**.md", "**.mdx", "**.png", "**.jpg"] env: - GO_VERSION: '1.25.12' + GO_VERSION: '1.26.6' jobs: detect-noop: @@ -27,7 +27,7 @@ jobs: steps: - name: Detect No-op Changes id: noop - uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5.3.1 + uses: fkirc/skip-duplicate-actions@b974a9395958c231af965b70070979a577efa578 # v5.3.2 with: github_token: ${{ secrets.GITHUB_TOKEN }} do_not_skip: '["workflow_dispatch", "schedule", "push"]' @@ -39,12 +39,12 @@ jobs: if: needs.detect-noop.outputs.noop != 'true' steps: - name: Set up Go - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 with: go-version: ${{ env.GO_VERSION }} - name: Check out code into the Go module directory - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # Fetch the history of all branches and tags. # This is needed for the test suite to switch between releases. @@ -141,12 +141,12 @@ jobs: if: needs.detect-noop.outputs.noop != 'true' steps: - name: Set up Go - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 with: go-version: ${{ env.GO_VERSION }} - name: Check out code into the Go module directory - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # Fetch the history of all branches and tags. # This is needed for the test suite to switch between releases. @@ -243,12 +243,12 @@ jobs: if: needs.detect-noop.outputs.noop != 'true' steps: - name: Set up Go - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 with: go-version: ${{ env.GO_VERSION }} - name: Check out code into the Go module directory - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: # Fetch the history of all branches and tags. # This is needed for the test suite to switch between releases. diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index 793bbcdcd..67febc7a6 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -27,7 +27,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Verify shellcheck is available run: shellcheck --version diff --git a/.golangci.yml b/.golangci.yml index 17ee4aafe..c1e6993c4 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,6 +1,6 @@ run: timeout: 15m - go: '1.25.12' + go: '1.26.6' linters-settings: stylecheck: diff --git a/.squad/templates/skills/humanizer/SKILL.md b/.squad/templates/skills/humanizer/SKILL.md new file mode 100644 index 000000000..4948f03d3 --- /dev/null +++ b/.squad/templates/skills/humanizer/SKILL.md @@ -0,0 +1,105 @@ +--- +name: "humanizer" +description: "Tone enforcement patterns for external-facing community responses" +domain: "communication, tone, community" +confidence: "low" +source: "manual (RFC #426 β€” PAO External Communications)" +--- + +## Context + +Use this skill whenever PAO drafts external-facing responses for issues or discussions. + +- Tone must be warm, helpful, and human-sounding β€” never robotic or corporate. +- Brady's constraint applies everywhere: **Humanized tone is mandatory**. +- This applies to **all external-facing content** drafted by PAO in Phase 1 issues/discussions workflows. + +## Patterns + +1. **Warm opening** β€” Start with acknowledgment ("Thanks for reporting this", "Great question!") +2. **Active voice** β€” "We're looking into this" not "This is being investigated" +3. **Second person** β€” Address the person directly ("you" not "the user") +4. **Conversational connectors** β€” "That said...", "Here's what we found...", "Quick note:" +5. **Specific, not vague** β€” "This affects the casting module in v0.8.x" not "We are aware of issues" +6. **Empathy markers** β€” "I can see how that would be frustrating", "Good catch!" +7. **Action-oriented closes** β€” "Let us know if that helps!" not "Please advise if further assistance is required" +8. **Uncertainty is OK** β€” "We're not 100% sure yet, but here's what we think is happening..." is better than false confidence +9. **Profanity filter** β€” Never include profanity, slurs, or aggressive language, even when quoting +10. **Baseline comparison** β€” Responses should align with tone of 5-10 "gold standard" responses (>80% similarity threshold) +11. **Empathetic disagreement** β€” "We hear you. That's a fair concern." before explaining the reasoning +12. **Information request** β€” Ask for specific details, not open-ended "can you provide more info?" +13. **No link-dumping** β€” Don't just paste URLs. Provide context: "Check out the getting started guide β€” specifically the section on routing" not just a bare link + +## Examples + +### 1. Welcome + +```text +Hey {author}! Welcome to Squad πŸ‘‹ Thanks for opening this. +{substantive response} +Let us know if you have questions β€” happy to help! +``` + +### 2. Troubleshooting + +```text +Thanks for the detailed report, {author}! +Here's what we think is happening: {explanation} +{steps or workaround} +Let us know if that helps, or if you're seeing something different. +``` + +### 3. Feature guidance + +```text +Great question! {context on current state} +{guidance or workaround} +We've noted this as a potential improvement β€” {tracking info if applicable}. +``` + +### 4. Redirect + +```text +Thanks for reaching out! This one is actually better suited for {correct location}. +{brief explanation of why} +Feel free to open it there β€” they'll be able to help! +``` + +### 5. Acknowledgment + +```text +Good catch, {author}. We've confirmed this is a real issue. +{what we know so far} +We'll update this thread when we have a fix. Thanks for flagging it! +``` + +### 6. Closing + +```text +This should be resolved in {version/PR}! πŸŽ‰ +{brief summary of what changed} +Thanks for reporting this, {author} β€” it made Squad better. +``` + +### 7. Technical uncertainty + +```text +Interesting find, {author}. We're not 100% sure what's causing this yet. +Here's what we've ruled out: {list} +We'd love more context if you have it β€” {specific ask}. +We'll dig deeper and update this thread. +``` + +## Anti-Patterns + +- ❌ Corporate speak: "We appreciate your patience as we investigate this matter" +- ❌ Marketing hype: "Squad is the BEST way to..." or "This amazing feature..." +- ❌ Passive voice: "It has been determined that..." or "The issue is being tracked" +- ❌ Dismissive: "This works as designed" without empathy +- ❌ Over-promising: "We'll ship this next week" without commitment from the team +- ❌ Empty acknowledgment: "Thanks for your feedback" with no substance +- ❌ Robot signatures: "Best regards, PAO" or "Sincerely, The Squad Team" +- ❌ Excessive emoji: More than 1-2 emoji per response +- ❌ Quoting profanity: Even when the original issue contains it, paraphrase instead +- ❌ Link-dumping: Pasting URLs without context ("See: https://...") +- ❌ Open-ended info requests: "Can you provide more information?" without specifying what information diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index b3c1c8c5f..08c85443e 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -50,6 +50,7 @@ Examples of unacceptable behavior include but are not limited to: * Other conduct which could reasonably be considered inappropriate in a professional setting The following behaviors are also prohibited: + * Providing knowingly false or misleading information in connection with a Code of Conduct investigation or otherwise intentionally tampering with an investigation. * Retaliating against a person because they reported an incident or provided information about an incident as a witness. @@ -65,7 +66,7 @@ permanently removed from the project team. ## Reporting Report abusive, harassing, or otherwise unacceptable behaviors in the KubeFleet community -to the project team at [kubefleet-maintainers@googlegroups.com](mailto:kubefleet-maintainers@googlegroups.com). +to the [KubeFleet maintainers](mailto:kubefleet@microsoft.com). All reports will be thoroughly reviewed and investigated, and a response will be prepared, as appropriate. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e8ba8dd4..3de2f5c1f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,3 +63,32 @@ Additive labels (stack on top of the above when applicable): - `ignore-for-release` β€” hides the PR entirely from auto-generated notes. Default to this for CI-only or internal-cleanup PRs with no user impact. PRs with no `release-note/*` label fall into "Other Changes" in the generated notes. Dependabot PRs are labeled `dependencies` automatically and land under "Maintenance and Dependencies" without a `release-note/*` label. + +## Backporting to release branches + +Fixes that must land in a supported release (see the support window in +[SECURITY.md](SECURITY.md)) are backported by cherry-picking the squash commit +from `main` onto the matching `release-0.Y` branch. Backports are automated by +[`backport.yml`](.github/workflows/backport.yml): + +1. Merge the fix to `main` first. Backports are always cherry-picks of a commit + already on `main`, never direct PRs against a release branch. +2. Add a `cherry-pick/0.Y` label to the PR β€” before or after the merge, one + label per target minor. On merge (or on labeling an already-merged PR), the + automation opens a backport PR against `release-0.Y`. + +The automation follows three explicit rules: + +- **Squash merges only.** It cherry-picks the PR's single squash commit. PRs + merged any other way (merge commit, multi-commit rebase) are skipped with a + comment and must be backported manually. +- **Conflicts are never pushed.** If the pick does not apply cleanly, it aborts + and comments manual instructions on the original PR; it never opens a PR with + conflict markers. Re-adding the label retries after you resolve the cause. +- **Bot branches are force-pushed.** `cherry-pick/0.Y/pr-` branches belong to + the automation and are overwritten on retries β€” do manual backports on your + own branch, not on a bot branch. + +Backport PRs keep the original commit's `Signed-off-by` (DCO) and gain a +`(cherry picked from commit ...)` trailer. Merging the backport PR into +`release-0.Y` is still subject to the usual review and CI gates. diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 4b0e8000f..d4bc29b98 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -1,11 +1,11 @@ -# The KubeFleet Maintainers - -| Maintainer | Organization | GitHub Username | -|------------------|--------------|----------------------------------------------------| -| Ryan Zhang | Microsoft | [@ryanzhang-oss](https://github.com/ryanzhang-oss) | -| Zhiying Lin | Microsoft | [@zhiying-lin](https://github.com/zhiying-lin) | -| Chen Yu | Microsoft | [@michaelawyu](https://github.com/michaelawyu) | -| Wei Weng | Microsoft | [@weng271190436](https://github.com/weng271190436) | -| Yetkin Timocin | Microsoft | [@ytimocin](https://github.com/ytimocin) | -| StΓ©phane Erbrech | Microsoft | [@serbrech](https://github.com/serbrech) | -| Simon Waight | Microsoft | [@sjwaight](https://github.com/sjwaight) | +# The KubeFleet Maintainers + +| Maintainer | Organization | GitHub Username | +|--------------------------|--------------|----------------------------------------------------| +| Chen Yu | Microsoft | [@michaelawyu](https://github.com/michaelawyu) | +| Britania Rodriguez Reyes | Microsoft | [britaniar](https://github.com/britaniar) | +| Zhiying Lin | Microsoft | [@zhiying-lin](https://github.com/zhiying-lin) | +| Wei Weng | Microsoft | [@weng271190436](https://github.com/weng271190436) | +| Yetkin Timocin | Microsoft | [@ytimocin](https://github.com/ytimocin) | +| StΓ©phane Erbrech | Microsoft | [@serbrech](https://github.com/serbrech) | +| Simon Waight | Microsoft | [@sjwaight](https://github.com/sjwaight) | diff --git a/Makefile b/Makefile index 529dbe33e..e6f60961a 100644 --- a/Makefile +++ b/Makefile @@ -311,6 +311,41 @@ helm-push: ## Package and push Helm charts to OCI registry rm -rf .helm-packages +# Directory and artifact names for the standalone CRD release tarball. +CRD_PACKAGE_DIR ?= _crd-package +CRD_PACKAGE_NAME ?= kubefleet-crds-$(TAG) +# Use sha256sum when available (Linux/CI), fall back to shasum (macOS). +SHA256SUM ?= $(shell command -v sha256sum >/dev/null 2>&1 && echo "sha256sum" || echo "shasum -a 256") + +.PHONY: crd-package +crd-package: ## Package the raw CRDs into a release tarball with a SHA-256 checksum + rm -rf $(CRD_PACKAGE_DIR) + mkdir -p $(CRD_PACKAGE_DIR)/crds/hub $(CRD_PACKAGE_DIR)/crds/member + # Source from the charts' CRD sets (symlinks into config/crd/bases) so the + # bundle mirrors exactly what each agent installs: hub-cluster CRDs under + # crds/hub, member-cluster CRDs under crds/member. cp -L dereferences the + # symlinks so the archive holds the raw CRD YAML, not dangling links. + cp -L charts/hub-agent/templates/crds/*.yaml $(CRD_PACKAGE_DIR)/crds/hub/ + cp -L charts/member-agent/templates/crds/*.yaml $(CRD_PACKAGE_DIR)/crds/member/ + tar -czf $(CRD_PACKAGE_DIR)/$(CRD_PACKAGE_NAME).tgz -C $(CRD_PACKAGE_DIR) crds + cd $(CRD_PACKAGE_DIR) && $(SHA256SUM) $(CRD_PACKAGE_NAME).tgz > $(CRD_PACKAGE_NAME).tgz.sha256 + @echo "Packaged CRDs into $(CRD_PACKAGE_DIR)/$(CRD_PACKAGE_NAME).tgz" + +.PHONY: crd-verify +crd-verify: ## Verify the chart CRD directories cover every CRD in config/crd/bases; note (chenyu1): kubefleet.dev CRDs are ignored for now until the implementation is completed. + @bases="$$(mktemp)"; charts="$$(mktemp)"; \ + ls config/crd/bases/ | grep -v '^placement\.kubefleet\.dev' | sort > "$$bases"; \ + { ls charts/hub-agent/templates/crds/; ls charts/member-agent/templates/crds/; } | sort > "$$charts"; \ + missing="$$(comm -3 "$$bases" "$$charts")"; \ + rm -f "$$bases" "$$charts"; \ + if [ -n "$$missing" ]; then \ + echo "ERROR: chart CRD directories are out of sync with config/crd/bases."; \ + echo "Left column = only in config/crd/bases; right column = only in the charts:"; \ + echo "$$missing"; \ + echo "If you added a CRD, symlink it into charts/hub-agent/templates/crds/ or charts/member-agent/templates/crds/."; \ + exit 1; \ + fi; \ + echo "crd-verify: chart CRD directories cover all CRDs in config/crd/bases" # By default, docker buildx create will pull image moby/buildkit:buildx-stable-1 and hit the too many requests error # diff --git a/README.md b/README.md index 2507a85d4..1b10e34dc 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,9 @@ You can reach the KubeFleet community and developers via the following channels: ## Community Meetings -March 2026: we're currently revamping our community call schedule and will have more to share soon. +We aim to hold one meeting per month. Community meetings for US/EU and APAC/India communities happen in alternate months. -Future plans will land on our [community repository](https://github.com/kubefleet-dev/community). +Please refer to the [calendar](https://zoom-lfx.platform.linuxfoundation.org/meetings/kubefleet?view=month) for the latest schedule.