Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 12 additions & 74 deletions .github/workflows/chart-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ name: Helm Chart Release
# tag as fallback) unless workflow_dispatch forced a release.
# - Resolves the next version from the ECR OCI repo (highest X.Y.Z
# tag + 1 — the artifact store is the version record,
# platform-gitops#1201), packages, pushes, advances the marker ref,
# mints the legacy git tag, contributes to one aggregated GitHub
# Release.
# platform-gitops#1201), packages, pushes (plus the X.Y.Z-<sha7>
# provenance alias), advances the marker ref, and notifies Dispatch.
# No git tags or GitHub Releases are minted — historical
# `chart/<name>/X.Y.Z` tags remain as the frozen fallback baseline.
#
# Caller has no inputs; auto-discovery handles everything. Each repo's
# `ci.yml` decides whether to call this workflow.
Expand Down Expand Up @@ -156,9 +157,7 @@ jobs:
set -euo pipefail

# The ECR OCI repo is the version record: next version is a
# patch bump of the highest X.Y.Z tag already pushed. Git tags
# are a legacy mirror, still minted below for deploy
# correlation but no longer read (platform-gitops#1201). A
# patch bump of the highest X.Y.Z tag already pushed. A
# missing repo or an empty tag list both mean first release.
latest=$(aws ecr describe-images \
--repository-name "charts/${NAME}" \
Expand All @@ -184,7 +183,6 @@ jobs:
patch=$((patch + 1))
done
version="${major}.${minor}.${patch}"
tag="chart/${NAME}/${version}"

# Stamp the resolved version into Chart.yaml before packaging
sed -i "s/^version:.*/version: ${version}/" "${DIR}/Chart.yaml"
Expand Down Expand Up @@ -221,40 +219,18 @@ jobs:
>/dev/null
} || echo "::warning::sha alias tag failed for charts/${NAME}:${version}-${SHORT_SHA}"

# Tag immediately after successful push (retry up to 3 times).
# If permanent failure the chart sits in ECR with no git tag —
# the next run's recovery loop will reclaim it.
git tag "$tag"
tag_pushed=false
for attempt in 1 2 3; do
if git push origin "$tag"; then
tag_pushed=true
break
fi
echo "::warning::Git tag push attempt ${attempt}/3 failed for ${tag}"
sleep 2
done

if [ "$tag_pushed" != true ]; then
echo "::error::Git tag push failed after 3 attempts for ${tag} — chart already in ECR!"
exit 1
fi

# Advance the per-chart release marker — one mutable ref, not
# a tag, so it is invisible to Kargo's tag enumeration and the
# GitHub create-webhook. The prepare job diffs against it to
# decide whether the chart needs a release.
git push --force origin "${GITHUB_SHA}:refs/releases/chart/${NAME}" \
|| echo "::warning::Marker ref push failed for refs/releases/chart/${NAME} — next run falls back to the legacy tag baseline"

mkdir -p release-info
echo "${NAME} ${version}" > "release-info/${NAME}.txt"
echo "version=${version}" >> "$GITHUB_OUTPUT"

# Direct publish signal to Dispatch (platform-gitops#1201) — the
# replacement for the create-webhook on the legacy git tag. Never
# fails the release; the EventBridge ECR-push backstop covers a
# missed notification.
# The publish signal (platform-gitops#1201). Never fails the
# release; the EventBridge ECR-push backstop covers a missed
# notification.
- name: Notify Dispatch
if: steps.release.outcome == 'success'
uses: pinpredict/.github/actions/notify-dispatch@main
Expand All @@ -264,53 +240,15 @@ jobs:
version: ${{ steps.release.outputs.version }}
secret: ${{ secrets.CI_WEBHOOK_SECRET }}

- name: Upload release info
if: steps.release.outcome == 'success'
uses: actions/upload-artifact@v7
with:
name: chart-release-info-${{ matrix.name }}
path: release-info/${{ matrix.name }}.txt
retention-days: 1

# Kept as the aggregate fail-gate (stable check name for branch
# protection). The GitHub release it used to create was keyed off the
# legacy chart/* git tag; Dispatch's Slack cards carry the per-service
# changelog now (platform-gitops#1201).
release-summary:
needs: package
if: ${{ !cancelled() && needs.package.result != 'skipped' }}
runs-on: ubuntu-latest
permissions:
contents: write
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v6

- name: Download chart-release-info artifacts
uses: actions/download-artifact@v7
with:
path: release-info
pattern: chart-release-info-*
merge-multiple: true

- name: Create aggregated GitHub release
run: |
set -euo pipefail
shopt -s nullglob
released=()
for f in release-info/*.txt; do
released+=("$(cat "$f")")
done

if [ ${#released[@]} -eq 0 ]; then
echo "No charts were released."
exit 0
fi

title="Charts: $(IFS=', '; echo "${released[*]}")"
first_name="${released[0]%% *}"
first_ver="${released[0]##* }"
gh release create "chart/${first_name}/${first_ver}" \
--generate-notes \
--title "$title"

- name: Fail if any package failed
if: needs.package.result != 'success'
run: |
Expand Down
91 changes: 13 additions & 78 deletions .github/workflows/docker-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ name: Docker Release
# store is the version record (platform-gitops#1201). Each successful
# build pushes the image, advances the `refs/releases/image/<name>`
# marker ref (the change-detection baseline for discover-services),
# creates an immutable git tag `image/<name>/X.Y.Z`, and contributes to
# a single aggregated GitHub Release at the end of the run. Dispatch
# still correlates on the per-service git tags via the `create` webhook;
# the tags and Releases go away once Dispatch is notified directly
# (#1201 dependency 2).
# and notifies Dispatch directly (signed POST via notify-dispatch;
# the EventBridge ECR-push backstop covers a missed call). No git tags
# or GitHub Releases are minted — the historical `image/<name>/X.Y.Z`
# tags remain in repos as the frozen fallback baseline only.

on:
workflow_call:
Expand Down Expand Up @@ -119,7 +118,6 @@ jobs:
BUILD_ARGS: ${{ matrix.build_args }}
ECR_REGISTRY: ${{ steps.ecr-login.outputs.registry }}
SHORT_SHA: ${{ github.sha }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Empty unless private-modules minted a token above; gates the
# BuildKit --secret so it's a no-op for every other caller.
GH_PRIVATE_TOKEN: ${{ steps.private-module-token.outputs.token }}
Expand All @@ -129,10 +127,8 @@ jobs:

# The ECR repo is the version record: next version is a patch
# bump of the highest X.Y.Z tag already pushed (the -<sha>
# sibling tags are filtered out). Git tags are a legacy mirror,
# still minted below for deploy correlation but no longer read
# (platform-gitops#1201). A missing repo or an empty tag list
# both mean first release.
# sibling tags are filtered out). A missing repo or an empty
# tag list both mean first release.
latest=$(aws ecr describe-images \
--repository-name "${ECR_REPO}" \
--filter tagStatus=TAGGED \
Expand All @@ -157,7 +153,6 @@ jobs:
patch=$((patch + 1))
done
version="${major}.${minor}.${patch}"
tag="image/${NAME}/${version}"

echo "Building ${ECR_REPO} v${version} from ${DOCKERFILE}${TARGET:+ (target=${TARGET})}"

Expand Down Expand Up @@ -204,40 +199,18 @@ jobs:

docker buildx build "${buildx_args[@]}" .

# Tag immediately after successful push (retry up to 3 times).
# If the tag push fails permanently the image is in ECR with no
# git tag — the next run's recovery loop will reclaim it.
git tag "$tag"
tag_pushed=false
for attempt in 1 2 3; do
if git push origin "$tag"; then
tag_pushed=true
break
fi
echo "::warning::Git tag push attempt ${attempt}/3 failed for ${tag}"
sleep 2
done

if [ "$tag_pushed" != true ]; then
echo "::error::Git tag push failed after 3 attempts for ${tag} — image already in ECR!"
exit 1
fi

# Advance the per-service release marker — one mutable ref,
# not a tag, so it is invisible to Kargo's tag enumeration and
# the GitHub create-webhook. discover-services diffs against
# it to decide whether the service needs a rebuild.
git push --force origin "${GITHUB_SHA}:refs/releases/image/${NAME}" \
|| echo "::warning::Marker ref push failed for refs/releases/image/${NAME} — next run falls back to the legacy tag baseline"

mkdir -p release-info
echo "${NAME} ${version}" > "release-info/${NAME}.txt"
echo "version=${version}" >> "$GITHUB_OUTPUT"

# Direct publish signal to Dispatch (platform-gitops#1201) — the
# replacement for the create-webhook on the legacy git tag. Never
# fails the release; the EventBridge ECR-push backstop covers a
# missed notification.
# The publish signal (platform-gitops#1201). Never fails the
# release; the EventBridge ECR-push backstop covers a missed
# notification.
- name: Notify Dispatch
if: steps.release.outcome == 'success'
uses: pinpredict/.github/actions/notify-dispatch@main
Expand All @@ -247,53 +220,15 @@ jobs:
version: ${{ steps.release.outputs.version }}
secret: ${{ secrets.CI_WEBHOOK_SECRET }}

- name: Upload release info
if: steps.release.outcome == 'success'
uses: actions/upload-artifact@v7
with:
name: release-info-${{ matrix.name }}
path: release-info/${{ matrix.name }}.txt
retention-days: 1

# Kept as the aggregate fail-gate (stable check name for branch
# protection). The GitHub release it used to create was keyed off the
# legacy image/* git tag; Dispatch's Slack cards carry the per-service
# changelog now (platform-gitops#1201).
release-summary:
needs: build
if: ${{ !cancelled() && needs.build.result != 'skipped' }}
runs-on: ubuntu-latest
permissions:
contents: write
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v6

- name: Download release-info artifacts
uses: actions/download-artifact@v7
with:
path: release-info
pattern: release-info-*
merge-multiple: true

- name: Create aggregated GitHub release
run: |
set -euo pipefail
shopt -s nullglob
released=()
for f in release-info/*.txt; do
released+=("$(cat "$f")")
done

if [ ${#released[@]} -eq 0 ]; then
echo "No services were released."
exit 0
fi

title="Release: $(IFS=', '; echo "${released[*]}")"
first_name="${released[0]%% *}"
first_ver="${released[0]##* }"
gh release create "image/${first_name}/${first_ver}" \
--generate-notes \
--title "$title"

- name: Fail if any build failed
if: needs.build.result != 'success'
run: |
Expand Down
5 changes: 2 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,10 @@ Both workflows use a three-shot retry pattern (try / sleep 30 / retry / sleep 60
- **Release marker refs**: `refs/releases/image/<name>` and `refs/releases/chart/<name>` — one mutable ref per service, force-advanced to the released SHA on every successful push. They are the change-detection baseline for `discover-services` and chart-release's prepare job (legacy `image|chart/<name>/X.Y.Z` tag is the fallback until a service releases once with the marker in place). Not tags, so they don't feed Kargo's tag enumeration or the GitHub `create` webhook. Readers must fetch them explicitly (`+refs/releases/*:refs/releases/*`).
- **Dispatch publish notification**: after each successful push, both release workflows call `actions/notify-dispatch` — a signed POST (service, version, full SHA, run URL) through the public webhook-forwarder `/dispatch/ci` route, HMAC'd with the org `CI_WEBHOOK_SECRET` (reaches reusable workflows via `secrets: inherit`). Warn-only on failure: the EventBridge ECR-push backstop covers a missed call.
- **Chart sha alias**: chart-release aliases the pushed OCI chart as `X.Y.Z-<sha7>` (via `aws ecr put-image` on the same manifest — allowed under tag immutability) so the ECR-push backstop can recover commit provenance for charts, mirroring the image tag pair. Semver-prerelease form, so Kargo chart Warehouses ignore it.
- Per-service image tag: `image/<name>/X.Y.Z` (immutable git tag, pushed after successful ECR push). **Legacy mirror** — the direct notification above supersedes the `create`-webhook correlation; the tags are removed in #1201's final step once the new signals are verified.
- Per-chart tag: `chart/<name>/X.Y.Z` — same legacy status.
- **No `image/*` / `chart/*` git tags and no GitHub Releases are minted** (platform-gitops#1201). The historical tags remain in service repos as the *frozen* fallback change-detection baseline — do not prune them until every service has released once with marker refs (the readers fall back to them). Do not reintroduce tag minting: it spams Kargo's git tag enumeration and the GitHub `create` webhook.
- Per-service config tag (Kargo freight for `<svc>-config` Warehouse): `vX.Y.Z+<svc>` — semver **build metadata** form (`+`), not pre-release (`-`). The `+` form passes `semverConstraint: ">=0.0.0"` cleanly; the pre-release form would require `>=0.0.0-0`. **This tag family stays** — it is genuine Kargo freight.

Pinpoint/Dispatch (our deploy correlator) hooks the GitHub `create` webhook on `image/<name>/*` tags — keep the tag format stable until the direct CI→Dispatch notification lands.
Dispatch (our deploy correlator) learns about publishes from the notify-dispatch POST plus the EventBridge ECR-push backstop; its legacy `create`-webhook handler remains but no longer receives artifact tags.

### `tag-config.yml` → platform-gitops dispatch

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ Why `.github` and not a dedicated `github-actions` repo: `.github` is *the* GitH

| File | Purpose |
|---|---|
| `docker-release.yml` | Matrix-based image build + push to ECR. Version = highest `X.Y.Z` tag in the ECR repo + 1 (ECR is the version record — platform-gitops#1201); advances the `refs/releases/image/<name>` marker ref, mints the legacy `image/<name>/X.Y.Z` git tag (Dispatch correlation, being phased out), aggregated GitHub Release. Caller passes a `matrix` input in the standard `{include:[...]}` shape. Optional `private-modules: true` mints a short-lived read-only `pinpredict-argocd` App token and exposes it to the build as BuildKit secret `id=gh_token` (`RUN --mount=type=secret,id=gh_token …`) — for Dockerfiles that fetch a private pinpredict module (e.g. `github.com/pinpredict/ppkit`) instead of vendoring it. Default false. |
| `chart-release.yml` | Auto-discovers `charts/*/`, skips charts unchanged since their `refs/releases/chart/<name>` marker ref, resolves the next version from the ECR OCI repo, packages, pushes, advances the marker, mints the legacy `chart/<name>/X.Y.Z` tag. No caller inputs. |
| `docker-release.yml` | Matrix-based image build + push to ECR. Version = highest `X.Y.Z` tag in the ECR repo + 1 (ECR is the version record — platform-gitops#1201); advances the `refs/releases/image/<name>` marker ref and notifies Dispatch. No git tags or GitHub Releases. Caller passes a `matrix` input in the standard `{include:[...]}` shape. Optional `private-modules: true` mints a short-lived read-only `pinpredict-argocd` App token and exposes it to the build as BuildKit secret `id=gh_token` (`RUN --mount=type=secret,id=gh_token …`) — for Dockerfiles that fetch a private pinpredict module (e.g. `github.com/pinpredict/ppkit`) instead of vendoring it. Default false. |
| `chart-release.yml` | Auto-discovers `charts/*/`, skips charts unchanged since their `refs/releases/chart/<name>` marker ref, resolves the next version from the ECR OCI repo, packages, pushes (+ `X.Y.Z-<sha7>` provenance alias), advances the marker, notifies Dispatch. No git tags or GitHub Releases. No caller inputs. |
| `tag-config.yml` | Tags merges to main that touch `.platform/services/<svc>.yaml` with `vX.Y.Z+<svc>` (per-service Kargo `<svc>-config` Warehouse freight), then dispatches `service-config-tag` to platform-gitops so missing pointer files get seeded. |
| `actionlint.yml` | Lints GitHub Actions workflow YAML with [`actionlint`](https://github.com/rhysd/actionlint) at a pinned version. Self-runs on this repo when PRs/pushes touch `.github/workflows/**` or `actions/**/action.yml`; callers reuse it via `uses: pinpredict/.github/.github/workflows/actionlint.yml@main`. |

Expand Down