From f7d904a9badc064ad5b4770f85fd892dca8747e7 Mon Sep 17 00:00:00 2001 From: Guanzhou Song Date: Mon, 9 Mar 2026 13:39:52 -0400 Subject: [PATCH 1/3] fix: repair Helm release publishing Publish the public Helm repository from an immutable source ref and add a reusable repair workflow for correcting released chart artifacts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Guanzhou Song --- .github/workflows/release_images.yml | 26 +- .../workflows/repair_helm_pages_release.yml | 316 ++++++++++++++++++ 2 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/repair_helm_pages_release.yml diff --git a/.github/workflows/release_images.yml b/.github/workflows/release_images.yml index 6a6f5ce3..d84487df 100644 --- a/.github/workflows/release_images.yml +++ b/.github/workflows/release_images.yml @@ -11,6 +11,9 @@ on: description: 'Version to release (used for images and Helm chart)' required: true default: '0.1.1' + source_ref: + description: 'Git ref to package the Helm chart from (tag or commit recommended to avoid drift)' + required: true run_tests: description: 'Run tests before releasing' required: false @@ -86,6 +89,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + ref: ${{ inputs.source_ref }} - name: Set up Helm uses: azure/setup-helm@v4 @@ -126,9 +131,28 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "- **Chart Name**: \`$CHART_NAME\`" >> $GITHUB_STEP_SUMMARY echo "- **Version**: \`$CHART_VERSION\` (unified for chart and images)" >> $GITHUB_STEP_SUMMARY + echo "- **Chart Source Ref**: \`${{ github.event.inputs.source_ref }}\`" >> $GITHUB_STEP_SUMMARY echo "- **Source Tag**: \`${{ github.event.inputs.candidate_version }}\`" >> $GITHUB_STEP_SUMMARY echo "- **Target Tag**: \`${{ github.event.inputs.version }}\`" >> $GITHUB_STEP_SUMMARY echo "- **Registry**: \`$GHCR_REPO\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Note**: Images promoted from \`${{ github.event.inputs.candidate_version }}\` to \`${{ github.event.inputs.version }}\` and Helm chart published" >> $GITHUB_STEP_SUMMARY - \ No newline at end of file + + publish-helm-pages: + name: Publish Helm Repository + needs: publish-helm-chart + if: ${{ always() && needs.publish-helm-chart.result == 'success' }} + permissions: + contents: write + uses: ./.github/workflows/repair_helm_pages_release.yml + with: + version: ${{ inputs.version }} + release_ref: ${{ inputs.source_ref }} + publish_branch: gh-pages + repo_url: https://documentdb.github.io/documentdb-kubernetes-operator + dry_run: false + confirm_version: ${{ inputs.version }} + # Follow the same gh-pages branch used by mike in deploy_docs.yml. + allow_pages_source_mismatch: true + secrets: inherit + diff --git a/.github/workflows/repair_helm_pages_release.yml b/.github/workflows/repair_helm_pages_release.yml new file mode 100644 index 00000000..a7d90118 --- /dev/null +++ b/.github/workflows/repair_helm_pages_release.yml @@ -0,0 +1,316 @@ +name: REPAIR - Republish Helm Chart to Pages + +on: + workflow_dispatch: + inputs: + version: + description: 'Released chart version to rebuild and republish' + required: true + default: '0.1.3' + release_ref: + description: 'Immutable git ref to rebuild from (tag recommended)' + required: true + default: '0.1.3' + expected_cnpg_version: + description: 'Expected cloudnative-pg dependency version in the rebuilt chart' + required: false + default: '0.23.2' + publish_branch: + description: 'Branch to update with the repaired Helm repository files' + required: true + default: 'gh-pages' + repo_url: + description: 'Public Helm repository base URL' + required: true + default: 'https://documentdb.github.io/documentdb-kubernetes-operator' + dry_run: + description: 'Build artifacts and upload them without pushing a commit' + required: true + default: true + type: boolean + confirm_version: + description: 'Safety check: repeat the version exactly before publishing' + required: true + default: '0.1.3' + allow_pages_source_mismatch: + description: 'Allow publishing even when GitHub Pages is configured from a different branch' + required: true + default: false + type: boolean + workflow_call: + inputs: + version: + description: 'Released chart version to rebuild and republish' + required: true + type: string + release_ref: + description: 'Immutable git ref to rebuild from (tag recommended)' + required: true + type: string + expected_cnpg_version: + description: 'Expected cloudnative-pg dependency version in the rebuilt chart' + required: false + default: '' + type: string + publish_branch: + description: 'Branch to update with the repaired Helm repository files' + required: false + default: 'gh-pages' + type: string + repo_url: + description: 'Public Helm repository base URL' + required: false + default: 'https://documentdb.github.io/documentdb-kubernetes-operator' + type: string + dry_run: + description: 'Build artifacts and upload them without pushing a commit' + required: false + default: false + type: boolean + confirm_version: + description: 'Safety check: repeat the version exactly before publishing' + required: true + type: string + allow_pages_source_mismatch: + description: 'Allow publishing even when GitHub Pages is configured from a different branch' + required: false + default: false + type: boolean + +permissions: + contents: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + repair-published-chart: + name: Repair Published Helm Chart + runs-on: ubuntu-latest + env: + CHART_DIR: operator/documentdb-helm-chart + + steps: + - name: Validate manual inputs + env: + VERSION: ${{ inputs.version }} + CONFIRM_VERSION: ${{ inputs.confirm_version }} + run: | + set -euo pipefail + if [[ "${VERSION}" != "${CONFIRM_VERSION}" ]]; then + echo "::error::confirm_version must exactly match version." + exit 1 + fi + + - name: Inspect GitHub Pages configuration + id: pages-config + uses: actions/github-script@v7 + with: + script: | + const result = await github.rest.repos.getPages({ + owner: context.repo.owner, + repo: context.repo.repo, + }); + const source = result.data.source || {}; + core.setOutput('branch', source.branch || ''); + core.setOutput('path', source.path || ''); + core.info(`GitHub Pages source: ${source.branch || '(unset)'}${source.path || ''}`); + + - name: Require explicit override for publish branch mismatch + if: ${{ !inputs.dry_run && !inputs.allow_pages_source_mismatch }} + env: + PAGES_SOURCE_BRANCH: ${{ steps.pages-config.outputs.branch }} + EXPECTED_BRANCH: ${{ inputs.publish_branch }} + run: | + set -euo pipefail + if [[ "${PAGES_SOURCE_BRANCH}" != "${EXPECTED_BRANCH}" ]]; then + echo "::error::GitHub Pages source branch is '${PAGES_SOURCE_BRANCH}', not '${EXPECTED_BRANCH}'. Review the Pages configuration before publishing or rerun with allow_pages_source_mismatch=true." + exit 1 + fi + + - name: Checkout release source + uses: actions/checkout@v4 + with: + ref: ${{ inputs.release_ref }} + fetch-depth: 0 + path: source + + - name: Checkout publish branch + uses: actions/checkout@v4 + with: + ref: ${{ inputs.publish_branch }} + fetch-depth: 0 + path: pages + + - name: Set up Helm + uses: azure/setup-helm@v4 + + - name: Validate chart source from release ref + working-directory: source + env: + VERSION: ${{ inputs.version }} + EXPECTED_CNPG_VERSION: ${{ inputs.expected_cnpg_version }} + CHART_PATH: ${{ env.CHART_DIR }}/Chart.yaml + CHART_LOCK_PATH: ${{ env.CHART_DIR }}/Chart.lock + run: | + set -euo pipefail + test -f "${CHART_PATH}" + test -f "${CHART_LOCK_PATH}" + ruby <<'RUBY' + require "yaml" + + chart = YAML.load_file(ENV.fetch("CHART_PATH")) + lock = YAML.load_file(ENV.fetch("CHART_LOCK_PATH")) + version = ENV.fetch("VERSION") + expected = ENV["EXPECTED_CNPG_VERSION"] + + abort("::error::Chart.yaml version #{chart["version"].inspect} does not match #{version.inspect}.") unless chart["version"] == version + + if expected && !expected.empty? + dependency = Array(lock["dependencies"]).find { |item| item["name"] == "cloudnative-pg" } + abort("::error::Chart.lock does not pin cloudnative-pg #{expected.inspect}.") unless dependency && dependency["version"] == expected + end + RUBY + helm dependency build "${CHART_DIR}" + + - name: Mirror current published chart artifacts + env: + REPO_URL: ${{ inputs.repo_url }} + run: | + set -euo pipefail + mkdir -p build live backups/live-artifacts + if curl -fsSL "${REPO_URL}/index.yaml" -o live/index.yaml; then + ruby <<'RUBY' > live/chart-urls.txt + require "yaml" + + data = YAML.load_file("live/index.yaml") + entries = data.fetch("entries", {}) + urls = entries.values.flatten.flat_map { |entry| Array(entry["urls"]) }.uniq + + puts urls + RUBY + else + cat > live/index.yaml <<'EOF' + apiVersion: v1 + entries: {} + EOF + : > live/chart-urls.txt + fi + while IFS= read -r url; do + if [[ ! "${url}" =~ ^https?:// ]]; then + relative_url="${url#./}" + relative_url="${relative_url#/}" + url="${REPO_URL%/}/${relative_url}" + fi + filename="$(basename "${url}")" + curl -fsSL "${url}" -o "backups/live-artifacts/${filename}" + cp "backups/live-artifacts/${filename}" "pages/${filename}" + done < live/chart-urls.txt + cp live/index.yaml backups/index.live.yaml + if [[ -f pages/index.yaml ]]; then + cp pages/index.yaml backups/index.from-branch.yaml + fi + + - name: Package corrected chart from release ref + working-directory: source + env: + VERSION: ${{ inputs.version }} + EXPECTED_CNPG_VERSION: ${{ inputs.expected_cnpg_version }} + PACKAGED_CHART_METADATA: ../build/packaged-chart-metadata.yaml + run: | + set -euo pipefail + helm package "${CHART_DIR}" --version "${VERSION}" --app-version "${VERSION}" --destination ../build + helm show chart "../build/documentdb-operator-${VERSION}.tgz" > "${PACKAGED_CHART_METADATA}" + ruby <<'RUBY' + require "yaml" + + chart = YAML.load_file(ENV.fetch("PACKAGED_CHART_METADATA")) + version = ENV.fetch("VERSION") + expected = ENV["EXPECTED_CNPG_VERSION"] + + abort("::error::Packaged chart version #{chart["version"].inspect} does not match #{version.inspect}.") unless chart["version"] == version + + if expected && !expected.empty? + dependency = Array(chart["dependencies"]).find { |item| item["name"] == "cloudnative-pg" } + abort("::error::Packaged chart does not include cloudnative-pg #{expected.inspect}.") unless dependency && dependency["version"] == expected + end + RUBY + + - name: Rebuild Helm repository files + env: + VERSION: ${{ inputs.version }} + REPO_URL: ${{ inputs.repo_url }} + run: | + set -euo pipefail + cp "build/documentdb-operator-${VERSION}.tgz" "pages/documentdb-operator-${VERSION}.tgz" + helm repo index pages --url "${REPO_URL}" --merge live/index.yaml + ruby <<'RUBY' + require "yaml" + + data = YAML.load_file("pages/index.yaml") + version = ENV.fetch("VERSION") + entries = Array(data.fetch("entries", {}).fetch("documentdb-operator", [])) + entry = entries.find { |item| item["version"] == version } + + abort("::error::Generated index.yaml is missing documentdb-operator #{version.inspect}.") unless entry + RUBY + + - name: Summarize repair preview + env: + VERSION: ${{ inputs.version }} + RELEASE_REF: ${{ inputs.release_ref }} + PUBLISH_BRANCH: ${{ inputs.publish_branch }} + REPO_URL: ${{ inputs.repo_url }} + DRY_RUN: ${{ inputs.dry_run }} + PAGES_SOURCE_BRANCH: ${{ steps.pages-config.outputs.branch }} + PAGES_SOURCE_PATH: ${{ steps.pages-config.outputs.path }} + run: | + set -euo pipefail + { + echo "## Helm Pages Repair Preview" + echo "" + echo "- Version: \`${VERSION}\`" + echo "- Release ref: \`${RELEASE_REF}\`" + echo "- Publish branch: \`${PUBLISH_BRANCH}\`" + echo "- GitHub Pages source: \`${PAGES_SOURCE_BRANCH}${PAGES_SOURCE_PATH}\`" + echo "- Repo URL: \`${REPO_URL}\`" + echo "- Dry run: \`${DRY_RUN}\`" + echo "" + echo "### Pending git changes" + echo '```text' + git -C pages status --short + echo '```' + } >> "${GITHUB_STEP_SUMMARY}" + + - name: Upload repair artifacts + uses: actions/upload-artifact@v4 + with: + name: helm-pages-repair-${{ inputs.version }} + path: | + build/documentdb-operator-${{ inputs.version }}.tgz + build/packaged-chart-metadata.yaml + live/index.yaml + live/chart-urls.txt + backups/** + pages/index.yaml + if-no-files-found: error + + - name: Publish repaired chart to GitHub Pages branch + if: ${{ !inputs.dry_run }} + working-directory: pages + env: + PUBLISH_BRANCH: ${{ inputs.publish_branch }} + run: | + set -euo pipefail + shopt -s nullglob + artifacts=(./*.tgz) + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add index.yaml "${artifacts[@]}" + if git diff --cached --quiet; then + echo "No changes to publish." + exit 0 + fi + git commit -m "fix: republish released Helm charts from live index and tagged source" + git push origin "HEAD:${PUBLISH_BRANCH}" From 8ecc7187c45b9c147182d2c0ed5899919119ec2b Mon Sep 17 00:00:00 2001 From: Guanzhou Song Date: Tue, 10 Mar 2026 10:50:31 -0400 Subject: [PATCH 2/3] fix: align helm pages publish with release packaging Make the reusable Helm Pages workflow support release-time chart metadata normalization so it can publish from the same immutable source ref as the GHCR chart release path. Also remove the invalid Chart.lock requirement and validate packaged appVersion explicitly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Guanzhou Song --- .github/workflows/release_images.yml | 1 + .../workflows/repair_helm_pages_release.yml | 47 ++++++++++++++++--- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release_images.yml b/.github/workflows/release_images.yml index d84487df..901accbf 100644 --- a/.github/workflows/release_images.yml +++ b/.github/workflows/release_images.yml @@ -152,6 +152,7 @@ jobs: repo_url: https://documentdb.github.io/documentdb-kubernetes-operator dry_run: false confirm_version: ${{ inputs.version }} + normalize_chart_metadata: true # Follow the same gh-pages branch used by mike in deploy_docs.yml. allow_pages_source_mismatch: true secrets: inherit diff --git a/.github/workflows/repair_helm_pages_release.yml b/.github/workflows/repair_helm_pages_release.yml index a7d90118..deab1263 100644 --- a/.github/workflows/repair_helm_pages_release.yml +++ b/.github/workflows/repair_helm_pages_release.yml @@ -32,6 +32,11 @@ on: description: 'Safety check: repeat the version exactly before publishing' required: true default: '0.1.3' + normalize_chart_metadata: + description: 'Allow packaging the checked-out source ref at the requested release version even when Chart.yaml still carries pre-release metadata' + required: false + default: false + type: boolean allow_pages_source_mismatch: description: 'Allow publishing even when GitHub Pages is configured from a different branch' required: true @@ -71,6 +76,11 @@ on: description: 'Safety check: repeat the version exactly before publishing' required: true type: string + normalize_chart_metadata: + description: 'Allow packaging the checked-out source ref at the requested release version even when Chart.yaml still carries pre-release metadata' + required: false + default: false + type: boolean allow_pages_source_mismatch: description: 'Allow publishing even when GitHub Pages is configured from a different branch' required: false @@ -151,28 +161,48 @@ jobs: env: VERSION: ${{ inputs.version }} EXPECTED_CNPG_VERSION: ${{ inputs.expected_cnpg_version }} + RELEASE_REF: ${{ inputs.release_ref }} + NORMALIZE_CHART_METADATA: ${{ inputs.normalize_chart_metadata }} CHART_PATH: ${{ env.CHART_DIR }}/Chart.yaml - CHART_LOCK_PATH: ${{ env.CHART_DIR }}/Chart.lock run: | set -euo pipefail test -f "${CHART_PATH}" - test -f "${CHART_LOCK_PATH}" ruby <<'RUBY' require "yaml" chart = YAML.load_file(ENV.fetch("CHART_PATH")) - lock = YAML.load_file(ENV.fetch("CHART_LOCK_PATH")) version = ENV.fetch("VERSION") expected = ENV["EXPECTED_CNPG_VERSION"] + release_ref = ENV.fetch("RELEASE_REF") + normalize_chart_metadata = ENV.fetch("NORMALIZE_CHART_METADATA") == "true" + source_version = chart["version"] - abort("::error::Chart.yaml version #{chart["version"].inspect} does not match #{version.inspect}.") unless chart["version"] == version + if source_version != version + if normalize_chart_metadata + puts "::notice::Chart.yaml version #{source_version.inspect} from #{release_ref.inspect} will be normalized to #{version.inspect} during packaging." + else + abort("::error::Chart.yaml version #{source_version.inspect} does not match #{version.inspect}. Rerun with normalize_chart_metadata=true if this source ref intentionally needs release metadata normalization.") + end + end if expected && !expected.empty? - dependency = Array(lock["dependencies"]).find { |item| item["name"] == "cloudnative-pg" } - abort("::error::Chart.lock does not pin cloudnative-pg #{expected.inspect}.") unless dependency && dependency["version"] == expected + dependency = Array(chart["dependencies"]).find { |item| item["name"] == "cloudnative-pg" } + abort("::error::Chart.yaml does not pin cloudnative-pg #{expected.inspect}.") unless dependency && dependency["version"] == expected end RUBY - helm dependency build "${CHART_DIR}" + + - name: Normalize chart metadata for packaged release + working-directory: source + env: + VERSION: ${{ inputs.version }} + CHART_PATH: ${{ env.CHART_DIR }}/Chart.yaml + VALUES_PATH: ${{ env.CHART_DIR }}/values.yaml + run: | + set -euo pipefail + sed -i "s/^version: .*/version: ${VERSION}/" "${CHART_PATH}" + sed -i "s/^appVersion: .*/appVersion: \"${VERSION}\"/" "${CHART_PATH}" + sed -i "s/tag: .*/tag: ${VERSION}/g" "${VALUES_PATH}" + helm dependency update "${CHART_DIR}" - name: Mirror current published chart artifacts env: @@ -230,6 +260,7 @@ jobs: expected = ENV["EXPECTED_CNPG_VERSION"] abort("::error::Packaged chart version #{chart["version"].inspect} does not match #{version.inspect}.") unless chart["version"] == version + abort("::error::Packaged chart appVersion #{chart["appVersion"].inspect} does not match #{version.inspect}.") unless chart["appVersion"] == version if expected && !expected.empty? dependency = Array(chart["dependencies"]).find { |item| item["name"] == "cloudnative-pg" } @@ -263,6 +294,7 @@ jobs: PUBLISH_BRANCH: ${{ inputs.publish_branch }} REPO_URL: ${{ inputs.repo_url }} DRY_RUN: ${{ inputs.dry_run }} + NORMALIZE_CHART_METADATA: ${{ inputs.normalize_chart_metadata }} PAGES_SOURCE_BRANCH: ${{ steps.pages-config.outputs.branch }} PAGES_SOURCE_PATH: ${{ steps.pages-config.outputs.path }} run: | @@ -276,6 +308,7 @@ jobs: echo "- GitHub Pages source: \`${PAGES_SOURCE_BRANCH}${PAGES_SOURCE_PATH}\`" echo "- Repo URL: \`${REPO_URL}\`" echo "- Dry run: \`${DRY_RUN}\`" + echo "- Normalize chart metadata: \`${NORMALIZE_CHART_METADATA}\`" echo "" echo "### Pending git changes" echo '```text' From 9f341059e6d301967cf7a4765d41c1cd16eb70e1 Mon Sep 17 00:00:00 2001 From: Guanzhou Song Date: Tue, 10 Mar 2026 11:08:36 -0400 Subject: [PATCH 3/3] fix: address critical issues in Helm release workflows - Remove dead sed for tag: field that doesn't exist in values.yaml (chart resolves image tags via Chart.appVersion, not a tag: key) - Add semver format validation on version input to prevent sed injection - Fix heredoc indentation bug that caused YAML parse error: replace unindented EOF heredoc delimiter with printf to stay within the YAML block scalar indentation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Guanzhou Song --- .github/workflows/release_images.yml | 5 ++--- .github/workflows/repair_helm_pages_release.yml | 11 +++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release_images.yml b/.github/workflows/release_images.yml index 901accbf..a28d2e34 100644 --- a/.github/workflows/release_images.yml +++ b/.github/workflows/release_images.yml @@ -97,9 +97,8 @@ jobs: - name: Update values.yaml with version tag run: | - echo "Updating values.yaml with version tag: ${{ github.event.inputs.version }}" - sed -i 's/tag: .*/tag: ${{ github.event.inputs.version }}/g' operator/documentdb-helm-chart/values.yaml - echo "Updated values.yaml content:" + echo "Chart uses Chart.appVersion for image tags (no tag: field in values.yaml)" + echo "values.yaml content:" cat operator/documentdb-helm-chart/values.yaml - name: Set chart version diff --git a/.github/workflows/repair_helm_pages_release.yml b/.github/workflows/repair_helm_pages_release.yml index deab1263..7e1f8366 100644 --- a/.github/workflows/repair_helm_pages_release.yml +++ b/.github/workflows/repair_helm_pages_release.yml @@ -108,6 +108,10 @@ jobs: CONFIRM_VERSION: ${{ inputs.confirm_version }} run: | set -euo pipefail + if [[ ! "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then + echo "::error::version must be a valid semver string (e.g. 0.1.3 or 0.2.0-rc.1)." + exit 1 + fi if [[ "${VERSION}" != "${CONFIRM_VERSION}" ]]; then echo "::error::confirm_version must exactly match version." exit 1 @@ -196,12 +200,10 @@ jobs: env: VERSION: ${{ inputs.version }} CHART_PATH: ${{ env.CHART_DIR }}/Chart.yaml - VALUES_PATH: ${{ env.CHART_DIR }}/values.yaml run: | set -euo pipefail sed -i "s/^version: .*/version: ${VERSION}/" "${CHART_PATH}" sed -i "s/^appVersion: .*/appVersion: \"${VERSION}\"/" "${CHART_PATH}" - sed -i "s/tag: .*/tag: ${VERSION}/g" "${VALUES_PATH}" helm dependency update "${CHART_DIR}" - name: Mirror current published chart artifacts @@ -221,10 +223,7 @@ jobs: puts urls RUBY else - cat > live/index.yaml <<'EOF' - apiVersion: v1 - entries: {} - EOF + printf 'apiVersion: v1\nentries: {}\n' > live/index.yaml : > live/chart-urls.txt fi while IFS= read -r url; do