@@ -17,12 +17,196 @@ permissions:
1717 contents : write
1818 pull-requests : write
1919
20+ concurrency :
21+ group : ${{ github.workflow }}-${{ github.ref }}
22+ # Use false for release workflows to prevent incomplete releases from cancellation
23+ cancel-in-progress : false
24+
2025jobs :
2126 update_release_draft :
27+ # Only run on the main repository, not forks
28+ if : github.repository == 'wallstop/unity-helpers'
2229 runs-on : ubuntu-latest
30+ timeout-minutes : 10
31+ outputs :
32+ version : ${{ steps.changelog.outputs.version }}
33+ release-id : ${{ steps.release_drafter.outputs.id }}
34+ release-url : ${{ steps.release_drafter.outputs.html_url }}
2335 steps :
24- - uses : release-drafter/release-drafter@v6
36+ - name : Checkout repository
37+ uses : actions/checkout@v4
38+ with :
39+ fetch-depth : 1 # Only need current commit for package.json and CHANGELOG.md
40+
41+ - name : Create changelog extraction script
42+ run : |
43+ set -euo pipefail
44+ cat > "${RUNNER_TEMP}/extract-changelog.awk" << '__AWKSCRIPT_HEREDOC_DELIMITER__'
45+ BEGIN { found = 0 }
46+ /^## \[/ {
47+ if (found) exit
48+ # Extract version from header: ## [X.Y.Z] or ## [X.Y.Z-prerelease+build]
49+ if (match($0, /\[[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?\]/)) {
50+ header_ver = substr($0, RSTART + 1, RLENGTH - 2)
51+ if (header_ver == target_ver) {
52+ found = 1
53+ next
54+ }
55+ }
56+ }
57+ found { print }
58+ __AWKSCRIPT_HEREDOC_DELIMITER__
59+
60+ - name : Extract changelog for version
61+ id : changelog
62+ run : |
63+ set -euo pipefail
64+
65+ # Validate required files exist
66+ if [ ! -f "package.json" ]; then
67+ echo "::error::package.json not found"
68+ exit 1
69+ fi
70+
71+ if [ ! -f "CHANGELOG.md" ]; then
72+ echo "::error::CHANGELOG.md not found"
73+ exit 1
74+ fi
75+
76+ if [ ! -f "${RUNNER_TEMP}/extract-changelog.awk" ]; then
77+ echo "::error::AWK script not found at ${RUNNER_TEMP}/extract-changelog.awk"
78+ exit 1
79+ fi
80+
81+ # Get the version from package.json with validation
82+ VERSION=$(jq -r '.version // empty' package.json)
83+
84+ if [ -z "$VERSION" ]; then
85+ echo "::error::Version field missing or empty in package.json"
86+ exit 1
87+ fi
88+
89+ # Validate semver format (X.Y.Z with optional prerelease/build metadata)
90+ if ! printf '%s' "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'; then
91+ echo "::error::Invalid semver format: $VERSION (expected X.Y.Z)"
92+ exit 1
93+ fi
94+
95+ echo "Extracting changelog for version: $VERSION"
96+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
97+
98+ # Extract changelog section using external AWK script with exact version matching
99+ awk -v target_ver="$VERSION" -f "${RUNNER_TEMP}/extract-changelog.awk" CHANGELOG.md > "${RUNNER_TEMP}/changelog_section.md"
100+
101+ # Check if we extracted any content
102+ if [ ! -s "${RUNNER_TEMP}/changelog_section.md" ]; then
103+ echo "::warning::No changelog section found for version $VERSION"
104+ fi
105+
106+ # Set multiline output using a random delimiter to prevent injection
107+ # Generate unique delimiter using date + random to avoid content collision
108+ DELIMITER="__CHANGELOG_EOF_$(date +%s%N)_${RANDOM}__"
109+ {
110+ printf 'content<<%s\n' "$DELIMITER"
111+ cat "${RUNNER_TEMP}/changelog_section.md"
112+ printf '%s\n' "$DELIMITER"
113+ } >> "$GITHUB_OUTPUT"
114+
115+ echo "Changelog section extracted successfully"
116+
117+ - name: Draft release
118+ id: release_drafter
119+ uses: release-drafter/release-drafter@v6
25120 with:
26121 config-name: release-drafter.yml
122+ version: ${{ steps.changelog.outputs.version }}
123+ tag: ${{ steps.changelog.outputs.version }}
27124 env:
28125 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
126+
127+ - name: Update release with changelog
128+ env:
129+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
130+ VERSION: ${{ steps.changelog.outputs.version }}
131+ CHANGELOG_CONTENT: ${{ steps.changelog.outputs.content }}
132+ RELEASE_ID: ${{ steps.release_drafter.outputs.id }}
133+ run: |
134+ set -euo pipefail
135+
136+ # Write changelog content to file using printf (avoids heredoc injection)
137+ # Using environment variable avoids shell interpolation issues
138+ printf '%s\n' "$CHANGELOG_CONTENT" > "${RUNNER_TEMP}/changelog_content.md"
139+
140+ API_STDERR="${RUNNER_TEMP}/gh_api_stderr.log"
141+
142+ # Validate release ID from release-drafter action
143+ if [ -z "$RELEASE_ID" ]; then
144+ echo "::error::release-drafter did not return a release ID"
145+ exit 1
146+ fi
147+
148+ echo "Using release ID $RELEASE_ID from release-drafter action"
149+
150+ # Fetch current release body and write to file for safe handling
151+ # Capture stderr to temp file for debugging instead of suppressing
152+ echo "Fetching current release body..."
153+ if ! gh api "repos/${{ github.repository }}/releases/$RELEASE_ID" \
154+ --jq '.body // ""' > "${RUNNER_TEMP}/current_body.md" 2>"$API_STDERR"; then
155+ echo "::warning::Failed to fetch current release body, using empty body"
156+ if [ -s "$API_STDERR" ]; then
157+ echo "::debug::gh api stderr: $(cat "$API_STDERR")"
158+ fi
159+ : > "${RUNNER_TEMP}/current_body.md"
160+ fi
161+
162+ # Check if changelog section already exists to prevent duplicate additions
163+ # Use -E for extended regex, -i for case-insensitive, allow optional leading whitespace
164+ if grep -qiE '^\s*## Changelog' "${RUNNER_TEMP}/current_body.md"; then
165+ echo "::notice::Changelog section already exists in release body, skipping update"
166+ echo "Release $RELEASE_ID already contains changelog for version $VERSION"
167+ exit 0
168+ fi
169+
170+ # Build new body using file concatenation (no embedded whitespace issues)
171+ {
172+ echo "## Changelog"
173+ echo ""
174+ cat "${RUNNER_TEMP}/changelog_content.md"
175+ echo ""
176+ cat "${RUNNER_TEMP}/current_body.md"
177+ } > "${RUNNER_TEMP}/new_body.md"
178+
179+ echo "Updating release with changelog..."
180+
181+ # Use file-based body to handle special characters safely
182+ # Retry with exponential backoff for transient API failures
183+ MAX_ATTEMPTS=3
184+ DELAY=2
185+ for attempt in $(seq 1 $MAX_ATTEMPTS); do
186+ if gh api "repos/${{ github.repository }}/releases/$RELEASE_ID" \
187+ -X PATCH \
188+ -F body=@"${RUNNER_TEMP}/new_body.md"; then
189+ echo "Release API update succeeded on attempt $attempt"
190+ break
191+ fi
192+ if [ "$attempt" -eq "$MAX_ATTEMPTS" ]; then
193+ echo "::error::Failed to update release body for release ID $RELEASE_ID after $MAX_ATTEMPTS attempts"
194+ exit 1
195+ fi
196+ echo "::warning::Attempt $attempt failed, retrying in ${DELAY}s..."
197+ sleep "$DELAY"
198+ DELAY=$((DELAY * 2))
199+ done
200+
201+ echo "Release $RELEASE_ID updated successfully with changelog for version $VERSION"
202+
203+ # Add step summary for workflow run visibility
204+ {
205+ echo "### ✅ Release Draft Updated"
206+ echo ""
207+ echo "| Property | Value |"
208+ echo "|----------|-------|"
209+ echo "| **Version** | \`$VERSION\` |"
210+ echo "| **Release ID** | \`$RELEASE_ID\` |"
211+ echo "| **Release URL** | ${{ steps.release_drafter.outputs.html_url }} |"
212+ } >> "$GITHUB_STEP_SUMMARY"
0 commit comments