Skip to content

Commit 3b3ffdb

Browse files
committed
ci: deploy a prior signed build's artifacts without rebuild
Extract release.yml's Deploy stage into a reusable workflow (release-deploy.yml, workflow_call + workflow_dispatch) parameterized by run-id: - release.yml calls it with the current run (integrated flow, unchanged behaviour) - dispatch it standalone with a prior no-deploy build's run-id to publish the exact same signed .nupkgs - "sign once, test locally, then deploy" with no rebuild Detects the version from the downloaded packages, tags the commit the artifacts were built from (resolved from the run, not the branch tip), and reuses the push/tag/release/bump steps. No duplicated deploy logic.
1 parent 4dad80c commit 3b3ffdb

3 files changed

Lines changed: 252 additions & 150 deletions

File tree

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
name: Release Deploy
2+
3+
# Deploys a previously-built release: downloads the `packages` artifact from a build run, pushes to NuGet.org,
4+
# tags the built commit, creates the GitHub Release, and (for a stable release) opens the next dev version.
5+
#
6+
# Two entry points share one job:
7+
# - workflow_call: release.yml's Deploy stage calls this with run-id = the current run (the integrated flow).
8+
# - workflow_dispatch: deploy a *prior* signed (no-deploy) build by its run id - "sign once, test locally, then
9+
# deploy the exact same artifacts" without a rebuild.
10+
#
11+
# The version is detected from the downloaded packages (not an input), so both entry points behave identically.
12+
# The tag points at the commit the artifacts were *built* from (resolved from the run), even if the branch has
13+
# since moved; the post-release version bump goes to the current branch tip.
14+
15+
on:
16+
workflow_call:
17+
inputs:
18+
run-id:
19+
description: Run whose 'packages' artifact to deploy
20+
required: true
21+
type: string
22+
bump-version:
23+
description: After a stable deploy, commit Patch+1 to the branch
24+
default: true
25+
type: boolean
26+
workflow_dispatch:
27+
inputs:
28+
run-id:
29+
description: Run ID of a prior signed (no-deploy) release build to deploy
30+
required: true
31+
type: string
32+
bump-version:
33+
description: After a stable deploy, commit Patch+1 to the branch (open next dev version)
34+
default: true
35+
type: boolean
36+
37+
jobs:
38+
Deploy:
39+
runs-on: ubuntu-latest # NuGet push doesn't need Windows
40+
environment: production # Requires manual approval in GitHub settings
41+
permissions:
42+
contents: write
43+
steps:
44+
- name: Check release permissions
45+
env:
46+
GH_TOKEN: ${{ secrets.GH_PAT }}
47+
run: |
48+
ORG=$(echo "${{ github.repository }}" | cut -d/ -f1)
49+
TEAM="stride-release-managers"
50+
USER="${{ github.actor }}"
51+
if ! gh api "orgs/$ORG/teams/$TEAM/memberships/$USER" --silent 2>/dev/null; then
52+
echo "::error::User $USER is not a member of $ORG/$TEAM. Deploy requires stride-release-managers team membership."
53+
exit 1
54+
fi
55+
56+
- name: Resolve built commit
57+
id: src
58+
env:
59+
GH_TOKEN: ${{ secrets.GH_PAT }}
60+
run: |
61+
SHA=$(gh run view "${{ inputs.run-id }}" --repo "${{ github.repository }}" --json headSha --jq .headSha)
62+
if [ -z "$SHA" ]; then
63+
echo "::error::Could not resolve the commit for run ${{ inputs.run-id }}"
64+
exit 1
65+
fi
66+
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
67+
echo "::notice::Deploying artifacts built from $SHA"
68+
69+
- uses: actions/checkout@v4
70+
with:
71+
fetch-depth: 0
72+
fetch-tags: true
73+
token: ${{ secrets.GH_PAT }} # push the tag and the post-release version-bump commit
74+
75+
- uses: actions/setup-dotnet@v4
76+
with:
77+
dotnet-version: '10.0.x'
78+
79+
- name: Download packages
80+
uses: actions/download-artifact@v4
81+
with:
82+
name: packages
83+
path: bin/packages
84+
run-id: ${{ inputs.run-id }}
85+
github-token: ${{ secrets.GH_PAT }}
86+
87+
- name: Detect version
88+
id: version
89+
shell: pwsh
90+
run: |
91+
$pkg = Get-ChildItem -Path bin/packages -Filter "Stride.Core.*.nupkg" | Select-Object -First 1
92+
if (-not $pkg) {
93+
echo "::error::No Stride.Core package found in run ${{ inputs.run-id }}"
94+
exit 1
95+
}
96+
$version = $pkg.Name -replace 'Stride\.Core\.(.*?)\.nupkg','$1'
97+
# Guard against a malformed build slipping a weird version into the package name.
98+
if ($version -notmatch '^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$') {
99+
echo "::error::Detected package version '$version' is not a valid version"
100+
exit 1
101+
}
102+
echo "version=$version" >> $env:GITHUB_OUTPUT
103+
echo "::notice::Package version: $version"
104+
105+
- name: Guard against re-publishing an existing version
106+
# The version is the committed value, bumped per release. If it wasn't bumped, its releases/<version> tag
107+
# already exists on a different commit than the one we're deploying - fail rather than re-publish.
108+
run: |
109+
TAG="releases/${{ steps.version.outputs.version }}"
110+
if git rev-parse "$TAG" >/dev/null 2>&1 && [ -z "$(git tag --points-at ${{ steps.src.outputs.sha }} --list "$TAG")" ]; then
111+
echo "::error::Version ${{ steps.version.outputs.version }} is already released (tag $TAG on another commit). Bump Patch/NuGetVersionSuffix in SharedAssemblyInfo.cs."
112+
exit 1
113+
fi
114+
115+
- name: List packages
116+
shell: pwsh
117+
run: |
118+
echo "## Packages to deploy" >> $env:GITHUB_STEP_SUMMARY
119+
echo '```' >> $env:GITHUB_STEP_SUMMARY
120+
Get-ChildItem -Path bin -Recurse -Filter "*.nupkg" | ForEach-Object {
121+
echo "$($_.Name)" >> $env:GITHUB_STEP_SUMMARY
122+
}
123+
echo '```' >> $env:GITHUB_STEP_SUMMARY
124+
125+
- name: Push NuGet packages
126+
shell: pwsh
127+
run: |
128+
# Content-versioned template packages (Samples, Starters) follow StrideSamplesVersion and
129+
# only change when the samples are bumped (see plans/samples-version-bump.md). Skip the
130+
# upload when that version is already on nuget.org - re-publishing the same version is a
131+
# no-op and these packages are large, so this avoids a wasted upload every release.
132+
function Test-NuGetPublished([string]$id, [string]$version) {
133+
$url = "https://api.nuget.org/v3-flatcontainer/$($id.ToLowerInvariant())/index.json"
134+
try { return ((Invoke-RestMethod -Uri $url -TimeoutSec 60 -ErrorAction Stop).versions -contains $version) }
135+
catch { return $false } # 404 (never published) or transient: fall through to push (--skip-duplicate stays safe)
136+
}
137+
138+
$packages = Get-ChildItem -Path bin/packages -Filter "*.nupkg"
139+
$contentVersionedIds = @('Stride.Templates.Samples', 'Stride.Templates.Games.Starters')
140+
$contentVersioned = $packages | Where-Object { $_.Name -match 'Stride\.Templates\.(Samples|Games\.Starters)\.' }
141+
$gameStudio = $packages | Where-Object { $_.Name -match 'GameStudio' }
142+
$main = $packages | Where-Object { $_.Name -notmatch 'GameStudio' -and $_.Name -notmatch 'Stride\.Templates\.(Samples|Games\.Starters)\.' }
143+
144+
echo "::group::Pushing main packages ($($main.Count))"
145+
foreach ($pkg in $main) {
146+
echo "Pushing $($pkg.Name)..."
147+
dotnet nuget push $pkg.FullName --api-key $env:STRIDE_NUGET_API_KEY --source "https://api.nuget.org/v3/index.json" --timeout 1800 --skip-duplicate
148+
}
149+
echo "::endgroup::"
150+
151+
if ($contentVersioned) {
152+
echo "::group::Pushing content-versioned template packages (Samples, Starters)"
153+
foreach ($pkg in $contentVersioned) {
154+
$id = $contentVersionedIds | Where-Object { $pkg.Name.StartsWith($_ + '.') } | Select-Object -First 1
155+
if ($id -and (Test-NuGetPublished $id $pkg.BaseName.Substring($id.Length + 1))) {
156+
echo "Skipping $($pkg.Name) (already published; samples unchanged since last release)."
157+
continue
158+
}
159+
echo "Pushing $($pkg.Name)..."
160+
dotnet nuget push $pkg.FullName --api-key $env:STRIDE_NUGET_API_KEY --source "https://api.nuget.org/v3/index.json" --timeout 1800 --skip-duplicate
161+
}
162+
echo "::endgroup::"
163+
}
164+
165+
if ($gameStudio) {
166+
echo "::group::Pushing GameStudio (last, so dependencies are already available)"
167+
foreach ($pkg in $gameStudio) {
168+
dotnet nuget push $pkg.FullName --api-key $env:STRIDE_NUGET_API_KEY --source "https://api.nuget.org/v3/index.json" --timeout 1800
169+
}
170+
echo "::endgroup::"
171+
}
172+
env:
173+
STRIDE_NUGET_API_KEY: ${{ secrets.STRIDE_NUGET_API_KEY }}
174+
175+
- name: Tag release
176+
id: tag
177+
run: |
178+
TAG="releases/${{ steps.version.outputs.version }}"
179+
if git rev-parse "$TAG" >/dev/null 2>&1; then
180+
echo "Tag $TAG already exists, skipping"
181+
echo "created=false" >> "$GITHUB_OUTPUT"
182+
else
183+
git tag "$TAG" "${{ steps.src.outputs.sha }}" # tag the commit the artifacts were built from
184+
git push origin "$TAG"
185+
echo "created=true" >> "$GITHUB_OUTPUT"
186+
fi
187+
188+
- name: Create GitHub Release
189+
env:
190+
GH_TOKEN: ${{ secrets.GH_PAT }}
191+
run: |
192+
gh release create "releases/${{ steps.version.outputs.version }}" \
193+
--title "Stride ${{ steps.version.outputs.version }}" \
194+
--target "${{ steps.src.outputs.sha }}" \
195+
--generate-notes \
196+
bin/packages/*.nupkg
197+
198+
- name: Bump version for next cycle
199+
# After a *stable* deploy, advance Patch in source so the branch opens the next dev version (the tag above
200+
# points at the just-released commit). Skipped for prereleases (they keep the same number) and idempotent
201+
# re-runs where the tag already existed. Opt out with the bump-version input. Pushes to the current branch
202+
# tip (github.ref_name), not the built commit, so it advances the branch even if it moved past the build.
203+
if: ${{ inputs.bump-version && steps.tag.outputs.created == 'true' && !contains(steps.version.outputs.version, '-') }}
204+
run: |
205+
RELEASED="${{ steps.version.outputs.version }}"
206+
FILE=sources/shared/SharedAssemblyInfo.cs
207+
git checkout "${{ github.ref_name }}"
208+
CUR=$(sed -n 's/.*public const string Patch = "\([0-9][0-9]*\)".*/\1/p' "$FILE")
209+
# Don't commit anything if Patch can't be parsed (e.g. the file's shape changed). The release already
210+
# succeeded; warn and skip - the forget-to-bump guard will catch the un-bumped version next release.
211+
if ! printf '%s' "$CUR" | grep -qE '^[0-9]+$'; then
212+
echo "::warning::Could not parse Patch from $FILE (got '$CUR'); skipping version bump - bump it manually."
213+
exit 0
214+
fi
215+
NEXT=$((CUR + 1))
216+
sed -i "s/\(public const string Patch = \)\"$CUR\"/\1\"$NEXT\"/" "$FILE"
217+
if ! grep -q "public const string Patch = \"$NEXT\";" "$FILE"; then
218+
echo "::warning::Version bump did not apply to $FILE; skipping - bump it manually."
219+
exit 0
220+
fi
221+
git config user.name "github-actions[bot]"
222+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
223+
# Commit only the version file (never -a), so an unexpected working-tree change can't ride along.
224+
git commit -m "Open ${RELEASED%.*}.$NEXT for development (released $RELEASED)" -- "$FILE"
225+
git pull --rebase origin "${{ github.ref_name }}"
226+
git push origin "HEAD:${{ github.ref_name }}"
227+
228+
- name: Publish summary
229+
if: success()
230+
shell: pwsh
231+
env:
232+
VERSION: ${{ steps.version.outputs.version }}
233+
REPO_URL: ${{ github.server_url }}/${{ github.repository }}
234+
run: |
235+
$v = $env:VERSION
236+
echo "## Published $v" >> $env:GITHUB_STEP_SUMMARY
237+
echo "" >> $env:GITHUB_STEP_SUMMARY
238+
echo "[GitHub Release]($env:REPO_URL/releases/tag/releases/$v)" >> $env:GITHUB_STEP_SUMMARY
239+
echo "" >> $env:GITHUB_STEP_SUMMARY
240+
echo "| Package | nuget.org |" >> $env:GITHUB_STEP_SUMMARY
241+
echo "|---|---|" >> $env:GITHUB_STEP_SUMMARY
242+
Get-ChildItem -Path bin/packages -Filter "*.nupkg" | Sort-Object Name | ForEach-Object {
243+
$id = $_.Name -replace "\.$([regex]::Escape($v))\.nupkg$", ''
244+
echo "| $id | https://www.nuget.org/packages/$id/$v |" >> $env:GITHUB_STEP_SUMMARY
245+
}

0 commit comments

Comments
 (0)