Skip to content

Commit 95ed6dd

Browse files
authored
Merge pull request #4 from VisActor/feat/vgraph-post-release-workflow
ci: add post-release workflow
2 parents 7010ddb + 19211f0 commit 95ed6dd

1 file changed

Lines changed: 226 additions & 0 deletions

File tree

.github/workflows/post-release.yml

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
name: Post release after develop synced
2+
3+
on:
4+
repository_dispatch:
5+
types: [develop-synced]
6+
workflow_dispatch:
7+
inputs:
8+
version:
9+
description: 'Release version, e.g. 1.2.3'
10+
required: true
11+
type: string
12+
13+
concurrency:
14+
group: post-release-${{ github.event.client_payload.version || github.event.inputs.version || github.run_id }}
15+
cancel-in-progress: false
16+
17+
jobs:
18+
post_release:
19+
runs-on: ubuntu-latest
20+
21+
permissions:
22+
contents: write
23+
24+
steps:
25+
- name: Read version from event payload or workflow_dispatch input
26+
id: meta
27+
env:
28+
PAYLOAD_VERSION: ${{ github.event.client_payload.version }}
29+
INPUT_VERSION: ${{ github.event.inputs.version }}
30+
run: |
31+
set -euo pipefail
32+
VERSION="${PAYLOAD_VERSION:-${INPUT_VERSION:-}}"
33+
if [ -z "${VERSION}" ]; then
34+
echo "No version in repository_dispatch payload or workflow_dispatch input, skip post-release."
35+
echo "skip=true" >> "$GITHUB_OUTPUT"
36+
exit 0
37+
fi
38+
echo "Using version: ${VERSION}"
39+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
40+
echo "skip=false" >> "$GITHUB_OUTPUT"
41+
42+
- name: Checkout main
43+
if: steps.meta.outputs.skip != 'true'
44+
uses: actions/checkout@v4
45+
with:
46+
ref: main
47+
fetch-depth: 0
48+
persist-credentials: false
49+
50+
- name: Configure git remote to use PAT
51+
if: steps.meta.outputs.skip != 'true'
52+
env:
53+
GH_PAT: ${{ secrets.CREATE_TAG_RELEASE_TOKEN }}
54+
run: |
55+
set -euo pipefail
56+
git remote set-url origin "https://x-access-token:${GH_PAT}@github.com/${GITHUB_REPOSITORY}.git"
57+
58+
- name: Fetch main and tags
59+
if: steps.meta.outputs.skip != 'true'
60+
run: |
61+
set -euo pipefail
62+
git fetch origin main:refs/remotes/origin/main --depth=1
63+
git fetch --tags --force
64+
65+
- name: Check existing tag and release
66+
id: exist
67+
if: steps.meta.outputs.skip != 'true'
68+
env:
69+
VERSION: ${{ steps.meta.outputs.version }}
70+
GH_TOKEN: ${{ secrets.CREATE_TAG_RELEASE_TOKEN }}
71+
run: |
72+
set -euo pipefail
73+
TAG="v${VERSION}"
74+
MAIN_SHA="$(git rev-parse origin/main)"
75+
76+
if gh release view "${TAG}" >/dev/null 2>&1; then
77+
echo "Release ${TAG} already exists, skip post-release."
78+
echo "skip=true" >> "$GITHUB_OUTPUT"
79+
echo "tag_exists=true" >> "$GITHUB_OUTPUT"
80+
exit 0
81+
fi
82+
83+
if git rev-parse "refs/tags/${TAG}" >/dev/null 2>&1; then
84+
TAG_SHA="$(git rev-list -n 1 "refs/tags/${TAG}")"
85+
if [ "${TAG_SHA}" != "${MAIN_SHA}" ]; then
86+
echo "Error: existing tag ${TAG} points to ${TAG_SHA}, expected origin/main ${MAIN_SHA}."
87+
exit 1
88+
fi
89+
90+
echo "Tag ${TAG} already exists at origin/main, continue creating the missing GitHub Release."
91+
echo "tag_exists=true" >> "$GITHUB_OUTPUT"
92+
else
93+
echo "tag_exists=false" >> "$GITHUB_OUTPUT"
94+
fi
95+
96+
echo "skip=false" >> "$GITHUB_OUTPUT"
97+
98+
- name: Ensure main changelog exists
99+
if: steps.meta.outputs.skip != 'true' && steps.exist.outputs.skip != 'true'
100+
run: |
101+
set -euo pipefail
102+
if [ ! -f "docs/assets/changelog/en/release.md" ]; then
103+
echo "Error: docs/assets/changelog/en/release.md not found in main."
104+
exit 1
105+
fi
106+
107+
- name: Extract release body from main changelog
108+
id: body
109+
if: steps.meta.outputs.skip != 'true' && steps.exist.outputs.skip != 'true'
110+
env:
111+
VERSION: ${{ steps.meta.outputs.version }}
112+
run: |
113+
set -euo pipefail
114+
node <<'NODE'
115+
const fs = require('fs');
116+
117+
const version = process.env.VERSION;
118+
const changelogPath = 'docs/assets/changelog/en/release.md';
119+
const content = fs.readFileSync(changelogPath, 'utf8');
120+
121+
function escapeRegExp(str) {
122+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
123+
}
124+
125+
const headerPattern = new RegExp('^#\\s*v?' + escapeRegExp(version) + '\\b', 'm');
126+
const match = headerPattern.exec(content);
127+
128+
if (!match) {
129+
console.error('No changelog block for version', version, 'found in main changelog.');
130+
process.exit(1);
131+
}
132+
133+
const startIndex = match.index;
134+
const rest = content.slice(startIndex);
135+
136+
const nextHeaderPattern = /^#\s*v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?[^\n]*$/gm;
137+
let nextIndex = rest.length;
138+
let m;
139+
while ((m = nextHeaderPattern.exec(rest)) !== null) {
140+
if (m.index > 0) {
141+
nextIndex = m.index;
142+
break;
143+
}
144+
}
145+
146+
const block = rest.slice(0, nextIndex).trimEnd() + '\n';
147+
if (!block.trim()) {
148+
console.error('Extracted changelog block is empty for version', version);
149+
process.exit(1);
150+
}
151+
fs.writeFileSync('release-body.md', block, 'utf8');
152+
NODE
153+
echo "has_body=true" >> "$GITHUB_OUTPUT"
154+
155+
- name: Validate extracted release body
156+
if: steps.meta.outputs.skip != 'true' && steps.exist.outputs.skip != 'true' && steps.body.outputs.has_body == 'true'
157+
env:
158+
VERSION: ${{ steps.meta.outputs.version }}
159+
run: |
160+
set -euo pipefail
161+
if [ ! -s "release-body.md" ]; then
162+
echo "Error: release-body.md is missing or empty."
163+
exit 1
164+
fi
165+
166+
if ! grep -Eq "^#\\s*v?${VERSION}\\b" release-body.md; then
167+
echo "Error: release-body.md does not start with expected version header v${VERSION}."
168+
exit 1
169+
fi
170+
171+
HEADER_COUNT="$(grep -Ec '^#\s*v?[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?\b' release-body.md || true)"
172+
if [ "${HEADER_COUNT}" -ne 1 ]; then
173+
echo "Error: extracted release body contains ${HEADER_COUNT} release headers, expected exactly 1."
174+
exit 1
175+
fi
176+
177+
- name: Verify gh identity
178+
if: steps.meta.outputs.skip != 'true' && steps.exist.outputs.skip != 'true' && steps.body.outputs.has_body == 'true'
179+
env:
180+
GH_TOKEN: ${{ secrets.CREATE_TAG_RELEASE_TOKEN }}
181+
run: |
182+
set -euo pipefail
183+
gh api user -q '.login'
184+
185+
- name: Diagnose PAT repository permission
186+
if: steps.meta.outputs.skip != 'true' && steps.exist.outputs.skip != 'true' && steps.body.outputs.has_body == 'true'
187+
env:
188+
GH_TOKEN: ${{ secrets.CREATE_TAG_RELEASE_TOKEN }}
189+
run: |
190+
set -euo pipefail
191+
LOGIN=$(gh api user -q '.login')
192+
echo "PAT user: $LOGIN"
193+
OWNER=${GITHUB_REPOSITORY%%/*}
194+
REPO=${GITHUB_REPOSITORY#*/}
195+
RESP=$(gh api "/repos/$OWNER/$REPO/collaborators/$LOGIN/permission" 2>&1 || true)
196+
echo "$RESP"
197+
if echo "$RESP" | grep -q '404'; then
198+
echo "Not a collaborator (404), repository access may be restricted or PAT not authorized to org"
199+
elif echo "$RESP" | grep -q '"permission":"write"'; then
200+
echo "permission ok: write"
201+
elif echo "$RESP" | grep -q '"permission":"admin"'; then
202+
echo "permission ok: admin"
203+
else
204+
echo "permission insufficient: $RESP"
205+
fi
206+
207+
- name: Create tag and GitHub Release
208+
if: steps.meta.outputs.skip != 'true' && steps.exist.outputs.skip != 'true' && steps.body.outputs.has_body == 'true'
209+
env:
210+
VERSION: ${{ steps.meta.outputs.version }}
211+
GH_TOKEN: ${{ secrets.CREATE_TAG_RELEASE_TOKEN }}
212+
run: |
213+
set -euo pipefail
214+
TAG="v${VERSION}"
215+
MAIN_SHA="$(git rev-parse origin/main)"
216+
217+
if [ "${{ steps.exist.outputs.tag_exists }}" != "true" ]; then
218+
echo "Creating tag ${TAG} at ${MAIN_SHA}"
219+
git tag "${TAG}" "${MAIN_SHA}"
220+
git push origin "${TAG}"
221+
else
222+
echo "Using existing tag ${TAG} at ${MAIN_SHA}"
223+
fi
224+
225+
echo "Creating GitHub Release ${TAG}"
226+
gh release create "${TAG}" --verify-tag --title "${TAG}" --notes-file "release-body.md"

0 commit comments

Comments
 (0)