-
Notifications
You must be signed in to change notification settings - Fork 19
ci: publish stack-aware preview images and charts for pull requests #810
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
7cb51c9
3887b59
9e16b1d
4335e2d
97aff82
b77a026
00efbc1
3722ad8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,273 @@ | ||
| name: PR Preview Chart | ||
|
|
||
| # Packages, pushes and signs a preview of deploy/chart for a pull request, | ||
| # versioned <chart version>-pr.<N>, into the PR registry project, and leaves a | ||
| # sticky comment with the install command. The chart-input allowlist lives in | ||
| # the `changes` job, not in a `paths` trigger filter: GitHub evaluates `paths` | ||
| # against the PR's own diff, which for a stacked PR is only the top slice. | ||
|
|
||
| on: | ||
| pull_request: | ||
| # `stacked`: GitHub opens the PRs of a stack before it links them, so | ||
| # `opened` never carries `pull_request.stack`; the link fires `stacked`. | ||
| types: [opened, synchronize, reopened, stacked] | ||
| branches: [main, "release-[0-9]*.[0-9]*"] | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: pr-chart-${{ github.event.pull_request.number || github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| env: | ||
| REGISTRY_ADDRESS: ${{ vars.REGISTRY_ADDRESS || '8gears.container-registry.com' }} | ||
| PR_REGISTRY_PROJECT: ${{ vars.PR_REGISTRY_PROJECT }} | ||
| RELEASE_PROJECT: ${{ vars.REGISTRY_PROJECT || '8gcr' }} | ||
|
|
||
| jobs: | ||
| changes: | ||
| name: Check chart inputs | ||
| # Forked PRs have no registry credentials and bot PRs need no preview. A | ||
| # chart release PR only restamps version and changelog. In a native | ||
| # GitHub stack only the top PR publishes: its head contains every lower PR. | ||
| if: >- | ||
| github.event.pull_request.head.repo.full_name == github.repository && | ||
| !contains(fromJSON('["8gcr-renovate[bot]", "renovate[bot]", "8gcr-sync[bot]"]'), github.actor) && | ||
| !startsWith(github.head_ref, 'release-please--') && | ||
| (github.event.pull_request.stack == null || | ||
| github.event.pull_request.stack.position == github.event.pull_request.stack.size) | ||
| runs-on: ubuntu-26.04 | ||
| timeout-minutes: 5 | ||
| outputs: | ||
| build: ${{ steps.diff.outputs.build }} | ||
| stack_number: ${{ github.event.pull_request.stack.number || '' }} | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| # Full history: the diff below spans the whole stack | ||
| fetch-depth: 0 | ||
| persist-credentials: false | ||
|
|
||
| - name: Diff against the stack base | ||
| id: diff | ||
| env: | ||
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | ||
| PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} | ||
| STACK_BASE_SHA: ${{ github.event.pull_request.stack.base.sha || '' }} | ||
| STACK_NUMBER: ${{ github.event.pull_request.stack.number || '' }} | ||
| EVENT_ACTION: ${{ github.event.action }} | ||
| # Only inputs that reach the packaged chart. | ||
| CHART_INPUTS: | | ||
| ^deploy/chart/ | ||
| ^\.github/workflows/pr-chart\.yml$ | ||
| ^\.github/scripts/chart-annotate-images\.sh$ | ||
| run: | | ||
| echo "event=${EVENT_ACTION} stack=${STACK_NUMBER:-none}" | ||
| base="${STACK_BASE_SHA:-${PR_BASE_SHA}}" | ||
| # --no-renames: a rename out of the allowlist would otherwise show | ||
| # only its new path and hide that a chart input went away | ||
| changed="$(git diff --name-only --no-renames "${base}...${HEAD_SHA}")" | ||
| if grep -Ef <(printf '%s' "${CHART_INPUTS}") <<<"${changed}"; then | ||
| echo "build=true" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "No chart input changed between ${base} and ${HEAD_SHA}; skipping the preview chart" | ||
| echo "build=false" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| preview-chart: | ||
| name: Publish preview chart | ||
| needs: [changes] | ||
| if: needs.changes.outputs.build == 'true' | ||
| runs-on: ubuntu-26.04 | ||
| timeout-minutes: 20 | ||
| permissions: | ||
| contents: read | ||
| # cosign signs with the workflow's OIDC identity | ||
| id-token: write | ||
| outputs: | ||
| version: ${{ steps.version.outputs.version }} | ||
| app_version: ${{ steps.version.outputs.app_version }} | ||
| digest: ${{ steps.push.outputs.digest }} | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| # The PR head, not the merge commit: the same ref the image previews build | ||
| ref: ${{ github.event.pull_request.head.sha }} | ||
| persist-credentials: false | ||
|
|
||
| - name: Load versions | ||
| # Helm homes: self-hosted runner images may ship a root-owned | ||
| # ~/.cache; keep all helm state in the always-writable temp. | ||
| run: | | ||
| grep -E '^[A-Z_]+=' versions.env >> "$GITHUB_ENV" | ||
| { | ||
| echo "HELM_CACHE_HOME=${RUNNER_TEMP}/helm/cache" | ||
| echo "HELM_CONFIG_HOME=${RUNNER_TEMP}/helm/config" | ||
| echo "HELM_DATA_HOME=${RUNNER_TEMP}/helm/data" | ||
| } >> "$GITHUB_ENV" | ||
|
|
||
| - name: Install Helm | ||
| uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1 | ||
| with: | ||
| version: ${{ env.HELM_VERSION }} | ||
|
|
||
| - name: Install envsubst | ||
| run: command -v envsubst || (sudo apt-get update && sudo apt-get install -y gettext-base) | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| - name: Install cosign | ||
| uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 | ||
|
|
||
| - name: Resolve the preview version | ||
| id: version | ||
| # <chart version>-pr.<N>: a semver prerelease, so helm accepts it as an | ||
| # exact --version and the OCI tag is the version string itself. It is | ||
| # overwritten on every push; the comment carries the digest for pinning. | ||
| env: | ||
| PR_NUMBER: ${{ github.event.pull_request.number }} | ||
| run: | | ||
| set -euo pipefail | ||
| read_key() { | ||
| awk -F'[:[:space:]]+' -v key="$1" '$1 == key { gsub(/["'"'"']/, "", $2); print $2; exit }' deploy/chart/Chart.yaml | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Custom agent: Enforce Strict Maintainability Standards When Prompt for AI agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not applied: both lines are copied verbatim from the release workflow, which has published every chart in this repository. Changing the preview alone would create the divergence the maintainability rule is meant to prevent; if the parser is wrong it is wrong in the release path first, and that is where it should be fixed. |
||
| } | ||
| version="$(read_key version)" | ||
| app_version="$(read_key appVersion)" | ||
| if [[ ! "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | ||
| echo "::error::Unusable chart version in deploy/chart/Chart.yaml: '${version}'" | ||
| exit 1 | ||
| fi | ||
| preview="${version}-pr.${PR_NUMBER}" | ||
| echo "PREVIEW_VERSION=${preview}" >> "$GITHUB_ENV" | ||
| { | ||
| echo "version=${preview}" | ||
| echo "app_version=${app_version}" | ||
| } >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Log in to registry | ||
| uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 | ||
| with: | ||
| registry: ${{ env.REGISTRY_ADDRESS }} | ||
| username: ${{ vars.PR_REGISTRY_USERNAME }} | ||
| password: ${{ secrets.PR_REGISTRY_PASSWORD }} | ||
|
|
||
| - name: Add per-release Artifact Hub images annotation | ||
| # The annotation names the images the chart defaults to: the released | ||
| # appVersion in the release project, not the preview project. | ||
| run: | | ||
| .github/scripts/chart-annotate-images.sh \ | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
|
||
| deploy/chart "${REGISTRY_ADDRESS}" "${RELEASE_PROJECT}" | ||
|
|
||
| - name: Build chart dependencies | ||
| # deploy/chart/charts/ is gitignored; Chart.lock pins the subcharts. | ||
| run: helm dependency build deploy/chart | ||
|
|
||
| - name: Package chart | ||
| # appVersion stays as committed, so default image tags resolve to a | ||
| # published Harbor release. | ||
| run: helm package deploy/chart --version "${PREVIEW_VERSION}" --destination dist | ||
|
|
||
| - name: Push chart | ||
| id: push | ||
| # pipefail: GitHub's default shell is `bash -e` without it, so a | ||
| # failing helm push would be masked by tee, and its error message can | ||
| # itself contain a sha256 digest the capture below would match. | ||
| run: | | ||
| set -euo pipefail | ||
| helm push "dist/harbor-next-${PREVIEW_VERSION}.tgz" \ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Custom agent: Enforce Strict Maintainability Standards This scrapes free-form Prompt for AI agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not applied: both lines are copied verbatim from the release workflow, which has published every chart in this repository. Changing the preview alone would create the divergence the maintainability rule is meant to prevent; if the parser is wrong it is wrong in the release path first, and that is where it should be fixed. |
||
| "oci://${REGISTRY_ADDRESS}/${PR_REGISTRY_PROJECT}/charts" 2>&1 | tee push.log | ||
| digest=$(grep -o 'sha256:[a-f0-9]\{64\}' push.log | head -1) | ||
| if [ -z "$digest" ]; then | ||
| echo "::error::Failed to capture the chart digest from helm push output" | ||
| exit 1 | ||
| fi | ||
| echo "digest=${digest}" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Sign chart | ||
| env: | ||
| CHART_DIGEST: ${{ steps.push.outputs.digest }} | ||
| run: | | ||
| cosign sign --yes \ | ||
| "${REGISTRY_ADDRESS}/${PR_REGISTRY_PROJECT}/charts/harbor-next@${CHART_DIGEST}" | ||
|
|
||
| pr-comment: | ||
| name: Comment preview chart ref | ||
| needs: [changes, preview-chart] | ||
| if: needs.changes.outputs.build == 'true' | ||
| runs-on: ubuntu-26.04 | ||
| permissions: | ||
| pull-requests: write | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: The comment job uses the Issues comments API but grants only Prompt for AI agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not applied, disproven in practice. Every preview comment in these repositories is posted by a job holding only |
||
| env: | ||
| PREVIEW_VERSION: ${{ needs.preview-chart.outputs.version }} | ||
| APP_VERSION: ${{ needs.preview-chart.outputs.app_version }} | ||
| DIGEST: ${{ needs.preview-chart.outputs.digest }} | ||
| STACK_NUMBER: ${{ needs.changes.outputs.stack_number }} | ||
| steps: | ||
| - name: Create or update PR comment | ||
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | ||
| env: | ||
| MARKER: "<!-- harbor-next-pr-chart-preview -->" | ||
| with: | ||
| script: | | ||
| const marker = process.env.MARKER; | ||
| const registryAddress = process.env.REGISTRY_ADDRESS; | ||
| const previewProject = process.env.PR_REGISTRY_PROJECT; | ||
| const chart = `${registryAddress}/${previewProject}/charts/harbor-next`; | ||
| const previewVersion = process.env.PREVIEW_VERSION; | ||
| const appVersion = process.env.APP_VERSION; | ||
| const digest = process.env.DIGEST; | ||
| const stackNumber = process.env.STACK_NUMBER; | ||
| const repository = process.env.GITHUB_REPOSITORY; | ||
| const issueNumber = context.payload.pull_request.number; | ||
| const { owner, repo } = context.repo; | ||
| const stackNote = stackNumber | ||
| ? `\nThis PR is the top of stack #${stackNumber}; the chart is packaged from its head and contains every PR in the stack.\n` | ||
| : ''; | ||
|
|
||
| const body = `${marker} | ||
| A preview chart for this PR is available: | ||
|
|
||
| \`\`\` | ||
| helm install harbor-next \\ | ||
| oci://${chart} \\ | ||
| --version ${previewVersion} | ||
| \`\`\` | ||
|
|
||
| Digest: \`${digest}\`. The chart defaults to Harbor \`${appVersion}\`; this PR's \`pr-${issueNumber}\` images in \`${previewProject}\`, when published, can be selected through the chart's image values. | ||
| ${stackNote} | ||
| Verify the preview chart: | ||
|
|
||
| \`\`\` | ||
| cosign verify \\ | ||
| --certificate-identity-regexp="https://github.com/${repository}/.github/workflows/pr-chart.yml@.*" \\ | ||
| --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \\ | ||
| ${chart}@${digest} | ||
| \`\`\``; | ||
|
|
||
| const comments = await github.paginate(github.rest.issues.listComments, { | ||
| owner, | ||
| repo, | ||
| issue_number: issueNumber, | ||
| per_page: 100, | ||
| }); | ||
|
|
||
| const existingComment = comments.find(comment => | ||
| comment.user?.login === 'github-actions[bot]' && | ||
| comment.body?.startsWith(marker) | ||
| ); | ||
|
|
||
| if (existingComment) { | ||
| await github.rest.issues.updateComment({ | ||
| owner, | ||
| repo, | ||
| comment_id: existingComment.id, | ||
| body, | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| await github.rest.issues.createComment({ | ||
| owner, | ||
| repo, | ||
| issue_number: issueNumber, | ||
| body, | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: When a native stack is being created, the
openedevent haspull_request.stack == null, so this condition lets every lower PR publish before thestackedevent arrives. Defer publication for stack creation or otherwise distinguish an unlinked stack from a plain PR to preserve the top-only guarantee.Prompt for AI agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not reachable for the members that matter. Until the stack is linked, every PR above the bottom has its base set to the branch below, which does not match the
branchesfilter, so no run starts. The bottom PR does run, and at that moment it is a plain PR targeting the trunk, which is the correct verdict. Observed on the stack that produced this design: the top PR got no run atopenedand its first run came fromstacked.