Skip to content

Commit 8487dff

Browse files
authored
feat: add reusable-promote workflow (C1 consolidation)
Consolidates promote-testing-to-main.yml from bluefin (343 LoC) + bluefin-lts (349 LoC) + dakota (183 LoC) into one 220-line reusable workflow. No new secrets. Includes gate job. release-state.yaml schema unchanged. Consumer validated: dakota#788.
1 parent 84649d2 commit 8487dff

1 file changed

Lines changed: 220 additions & 0 deletions

File tree

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# reusable-promote.yml — shared promote-testing-to-main logic
2+
#
3+
# Consolidates the per-image promote-testing-to-main.yml workflows:
4+
# projectbluefin/bluefin (~343 LoC) → thin caller (~30 LoC)
5+
# projectbluefin/bluefin-lts (~349 LoC) → thin caller (~30 LoC)
6+
# projectbluefin/dakota (~183 LoC) → thin caller (~30 LoC)
7+
#
8+
# Implements consistency-audit C1 from projectbluefin/common.
9+
10+
name: reusable-promote
11+
12+
on:
13+
workflow_call:
14+
inputs:
15+
variants:
16+
description: >-
17+
JSON array of variant names,
18+
e.g. '["dakota","dakota-nvidia"]'
19+
required: true
20+
type: string
21+
cosign_identity_regexp:
22+
description: >-
23+
Regex for cosign certificate identity,
24+
e.g. '^https://github\.com/projectbluefin/(dakota|actions)/\.github/workflows/'
25+
required: true
26+
type: string
27+
registry:
28+
description: 'GHCR registry prefix'
29+
required: false
30+
type: string
31+
default: 'ghcr.io/projectbluefin'
32+
run_e2e:
33+
description: 'Whether the release-gate should require a passing e2e run'
34+
required: false
35+
type: boolean
36+
default: false
37+
e2e_suite:
38+
description: 'testsuite suite identifier (smoke, common, lts, dakota)'
39+
required: false
40+
type: string
41+
default: 'smoke'
42+
lts_floor_days:
43+
description: 'Min days since last promotion before allowing a new one (LTS=7, others=0)'
44+
required: false
45+
type: number
46+
default: 0
47+
48+
permissions:
49+
contents: read
50+
51+
concurrency:
52+
group: promote-testing-to-main-${{ github.event.repository.name }}
53+
cancel-in-progress: false
54+
55+
jobs:
56+
promote:
57+
name: Resolve digests and maintain promotion PR
58+
runs-on: ubuntu-latest
59+
timeout-minutes: 15
60+
permissions:
61+
contents: write
62+
packages: read
63+
pull-requests: write
64+
outputs:
65+
changed: ${{ steps.branch.outputs.changed }}
66+
pr_number: ${{ steps.pr.outputs.pr_number }}
67+
steps:
68+
- name: Checkout
69+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
70+
with:
71+
fetch-depth: 0
72+
ref: main
73+
74+
- name: Configure git identity
75+
run: |
76+
git config user.name "github-actions[bot]"
77+
git config user.email "github-actions[bot]@users.noreply.github.com"
78+
79+
- name: Apply LTS floor (skip if promoted too recently)
80+
if: inputs.lts_floor_days > 0
81+
id: lts-floor
82+
run: |
83+
set -euo pipefail
84+
LAST=$(git log -1 --format=%ct origin/main -- .github/release-state.yaml 2>/dev/null || echo 0)
85+
NOW=$(date +%s)
86+
AGE=$(( (NOW - LAST) / 86400 ))
87+
FLOOR=${{ inputs.lts_floor_days }}
88+
if [ "$AGE" -lt "$FLOOR" ]; then
89+
echo "::notice::LTS floor not met: ${AGE}d < ${FLOOR}d — skipping"
90+
echo "skip=true" >> "$GITHUB_OUTPUT"
91+
else
92+
echo "skip=false" >> "$GITHUB_OUTPUT"
93+
fi
94+
95+
- name: Authenticate to GHCR for skopeo
96+
if: steps.lts-floor.outputs.skip != 'true'
97+
env:
98+
GH_TOKEN: ${{ github.token }}
99+
run: |
100+
echo "$GH_TOKEN" | skopeo login ghcr.io \
101+
--username "x-access-token" \
102+
--password-stdin
103+
104+
- name: Resolve current :testing digests
105+
if: steps.lts-floor.outputs.skip != 'true'
106+
id: digests
107+
env:
108+
REGISTRY: ${{ inputs.registry }}
109+
VARIANTS: ${{ inputs.variants }}
110+
run: |
111+
set -euo pipefail
112+
{
113+
printf '# Managed by reusable-promote.yml — do not edit manually.\n'
114+
printf '# Merging the PR that updates this file promotes these digests to :stable.\n'
115+
printf 'testing:\n'
116+
echo "${VARIANTS}" | jq -r '.[]' | while read -r variant; do
117+
digest=$(skopeo inspect \
118+
--format '{{.Digest}}' \
119+
"docker://${REGISTRY}/${variant}:testing")
120+
if [ -z "$digest" ]; then
121+
echo "::error::Could not resolve digest for ${variant}:testing"
122+
exit 1
123+
fi
124+
printf ' %s: "%s"\n' "${variant}" "${digest}"
125+
done
126+
printf 'updated_at: "%s"\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
127+
} > /tmp/release-state.yaml
128+
129+
- name: Update or create promotion branch
130+
if: steps.lts-floor.outputs.skip != 'true'
131+
id: branch
132+
env:
133+
GH_TOKEN: ${{ github.token }}
134+
PROMOTION_BRANCH: auto/promote-testing-to-main
135+
run: |
136+
set -euo pipefail
137+
git fetch origin "${PROMOTION_BRANCH}" 2>/dev/null || true
138+
git checkout -B "${PROMOTION_BRANCH}" origin/main
139+
140+
mkdir -p .github
141+
cp /tmp/release-state.yaml .github/release-state.yaml
142+
git add .github/release-state.yaml
143+
144+
if git diff --cached --quiet; then
145+
echo "::notice::No digest changes — nothing to do"
146+
echo "changed=false" >> "$GITHUB_OUTPUT"
147+
exit 0
148+
fi
149+
150+
git commit -m "ci: update testing image digests for stable promotion"
151+
git push --force-with-lease origin "${PROMOTION_BRANCH}"
152+
echo "changed=true" >> "$GITHUB_OUTPUT"
153+
154+
- name: Open or update promotion PR
155+
if: steps.branch.outputs.changed == 'true'
156+
id: pr
157+
env:
158+
GH_TOKEN: ${{ github.token }}
159+
PROMOTION_BRANCH: auto/promote-testing-to-main
160+
VARIANTS: ${{ inputs.variants }}
161+
run: |
162+
set -euo pipefail
163+
FIRST_DIGEST=$(grep -m1 '^ [a-z]' .github/release-state.yaml \
164+
| awk '{print $2}' | tr -d '"' | cut -c8-23)
165+
PR_TITLE="ci: promote testing images to stable (${FIRST_DIGEST})"
166+
167+
{
168+
echo "## Promote :testing → :stable"
169+
echo ""
170+
echo "Merge this PR to publish a stable release."
171+
echo ""
172+
echo '```yaml'
173+
cat .github/release-state.yaml
174+
echo '```'
175+
} > /tmp/pr-body.md
176+
177+
EXISTING=$(gh pr list \
178+
--repo "${{ github.repository }}" \
179+
--head "${PROMOTION_BRANCH}" \
180+
--base main \
181+
--state open \
182+
--json number \
183+
--jq '.[0].number // empty')
184+
185+
if [ -n "$EXISTING" ]; then
186+
gh pr edit "${EXISTING}" \
187+
--title "${PR_TITLE}" \
188+
--body-file /tmp/pr-body.md
189+
echo "pr_number=${EXISTING}" >> "$GITHUB_OUTPUT"
190+
else
191+
PR_URL=$(gh pr create \
192+
--repo "${{ github.repository }}" \
193+
--head "${PROMOTION_BRANCH}" \
194+
--base main \
195+
--title "${PR_TITLE}" \
196+
--body-file /tmp/pr-body.md)
197+
echo "pr_number=${PR_URL##*/}" >> "$GITHUB_OUTPUT"
198+
fi
199+
200+
gate:
201+
name: Release gate checks
202+
needs: [promote]
203+
if: needs.promote.outputs.changed == 'true'
204+
permissions:
205+
actions: read
206+
contents: read
207+
issues: write
208+
packages: read
209+
pull-requests: write
210+
uses: projectbluefin/actions/.github/workflows/reusable-release-gate.yml@84649d23ac4dbd9f7534c1490d8a80fb05ac2ab8 # v1
211+
with:
212+
repo: ${{ github.repository }}
213+
pr_number: ${{ needs.promote.outputs.pr_number }}
214+
registry: ${{ inputs.registry }}
215+
variants: ${{ inputs.variants }}
216+
cosign_identity_regexp: ${{ inputs.cosign_identity_regexp }}
217+
target_tag: testing
218+
run_e2e: ${{ inputs.run_e2e }}
219+
e2e_suites: ${{ inputs.e2e_suite }}
220+
secrets: inherit

0 commit comments

Comments
 (0)