Skip to content

Commit 76e0ba1

Browse files
authored
Merge pull request #139 from linearis-oss/feat/issue-52-release-automation
feat(release): automate weekly/dispatch releases and changelog ownership
2 parents 980156b + 0964e75 commit 76e0ba1

16 files changed

Lines changed: 10922 additions & 2478 deletions

.github/workflows/ci.yml

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ jobs:
112112
env:
113113
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
114114
run: |
115-
plan_files=$(git log main...HEAD \
115+
base_ref="origin/${GITHUB_BASE_REF:-main}"
116+
plan_files=$(git log "$base_ref"...HEAD \
116117
--diff-filter=A --name-only --pretty=format: \
117118
-- 'docs/plans/*.md' | sed '/^$/d')
118119
@@ -140,3 +141,37 @@ jobs:
140141
exit 1
141142
fi
142143
echo "No plan files found — OK"
144+
145+
guard-changelog-history:
146+
name: Guard CHANGELOG history in PR
147+
runs-on: ubuntu-latest
148+
if: github.event_name == 'pull_request'
149+
permissions:
150+
contents: read
151+
steps:
152+
- uses: actions/checkout@v6
153+
with:
154+
fetch-depth: 0
155+
156+
- name: Detect CHANGELOG.md in branch history
157+
shell: bash
158+
run: |
159+
set -euo pipefail
160+
161+
if [ "${GITHUB_HEAD_REF:-}" = "next" ] && [ "${GITHUB_BASE_REF:-}" = "main" ]; then
162+
echo "Promotion PR next -> main detected. Skipping changelog history guard."
163+
exit 0
164+
fi
165+
166+
base_ref="origin/${GITHUB_BASE_REF:-main}"
167+
output=$(git log "$base_ref"...HEAD --name-status --pretty=format: -- CHANGELOG.md | sed '/^$/d')
168+
169+
if [ -n "$output" ]; then
170+
echo "::error::CHANGELOG.md is release-workflow-owned and must not appear in PR branch history"
171+
echo "Drop or amend commits that touch CHANGELOG.md, then push with --force-with-lease"
172+
echo
173+
echo "$output"
174+
exit 1
175+
fi
176+
177+
echo "No CHANGELOG.md history violations — OK"
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
name: Promote next to main
2+
3+
on:
4+
push:
5+
branches:
6+
- next
7+
release:
8+
types:
9+
- published
10+
workflow_dispatch:
11+
12+
permissions:
13+
contents: read
14+
pull-requests: write
15+
16+
concurrency:
17+
group: promote-next-to-main
18+
cancel-in-progress: true
19+
20+
jobs:
21+
promote:
22+
if: ${{ github.event_name != 'release' || (github.event.release.prerelease == true && github.event.release.target_commitish == 'next') }}
23+
runs-on: ubuntu-latest
24+
steps:
25+
- name: Checkout code
26+
uses: actions/checkout@v6
27+
with:
28+
ref: next
29+
fetch-depth: 0
30+
31+
- name: Fetch main and next refs
32+
run: git fetch origin main next
33+
34+
- name: Build PR metadata
35+
id: meta
36+
shell: bash
37+
run: |
38+
set -euo pipefail
39+
40+
version=$(node -p "require('./package.json').version.replace(/-next\\.\\d+$/, '')")
41+
title="release ${version} to stable channel"
42+
compare_url="https://github.com/${GITHUB_REPOSITORY}/compare/main...next"
43+
44+
commits=$(git log --no-merges --pretty='- %h %s' origin/main..origin/next | head -n 200)
45+
if [ -z "$commits" ]; then
46+
commits="- (no new commits; branches currently aligned)"
47+
fi
48+
49+
{
50+
echo "title=$title"
51+
echo "version=$version"
52+
echo "compare_url=$compare_url"
53+
echo "body<<EOF"
54+
echo "## Purpose"
55+
echo "Promote tested prerelease changes from \\`next\\` into stable channel on \\`main\\`."
56+
echo
57+
echo "## Target stable version"
58+
echo "\\`$version\\`"
59+
echo
60+
echo "## Included commits (next ahead of main)"
61+
echo "$commits"
62+
echo
63+
echo "## Compare"
64+
echo "$compare_url"
65+
echo
66+
echo "## Notes"
67+
echo "- This PR is auto-maintained on every push to \\`next\\`."
68+
echo "- Merge this PR to promote current prerelease train to stable."
69+
echo "EOF"
70+
} >> "$GITHUB_OUTPUT"
71+
72+
- name: Upsert promotion PR
73+
env:
74+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
75+
shell: bash
76+
run: |
77+
set -euo pipefail
78+
79+
existing=$(gh pr list \
80+
--state open \
81+
--base main \
82+
--head next \
83+
--json number \
84+
--jq '.[0].number // empty')
85+
86+
if [ -n "$existing" ]; then
87+
gh pr edit "$existing" \
88+
--title "${{ steps.meta.outputs.title }}" \
89+
--body "${{ steps.meta.outputs.body }}"
90+
echo "Updated PR #$existing"
91+
else
92+
gh pr create \
93+
--base main \
94+
--head next \
95+
--title "${{ steps.meta.outputs.title }}" \
96+
--body "${{ steps.meta.outputs.body }}"
97+
echo "Created promotion PR"
98+
fi

.github/workflows/publish.yml

Lines changed: 0 additions & 111 deletions
This file was deleted.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
branches:
6+
- next
7+
schedule:
8+
- cron: "0 9 * * 1"
9+
workflow_dispatch:
10+
11+
permissions:
12+
contents: write
13+
id-token: write
14+
15+
concurrency:
16+
group: release-${{ github.event_name == 'schedule' && 'main' || github.ref_name }}
17+
cancel-in-progress: false
18+
19+
jobs:
20+
release:
21+
runs-on: ubuntu-latest
22+
steps:
23+
- name: Guard workflow_dispatch caller permissions
24+
if: ${{ github.event_name == 'workflow_dispatch' }}
25+
uses: actions/github-script@v8
26+
with:
27+
script: |
28+
const { owner, repo } = context.repo;
29+
const username = context.actor;
30+
const result = await github.rest.repos.getCollaboratorPermissionLevel({
31+
owner,
32+
repo,
33+
username,
34+
});
35+
36+
const role = result.data.role_name ?? result.data.permission;
37+
const allowed = new Set(["maintain", "admin"]);
38+
39+
if (!allowed.has(role)) {
40+
core.setFailed(
41+
`User ${username} has role '${role}'. Only maintain/admin may invoke workflow_dispatch releases.`,
42+
);
43+
return;
44+
}
45+
46+
core.info(`workflow_dispatch authorized for ${username} (${role})`);
47+
48+
- name: Resolve and validate target branch
49+
id: target
50+
shell: bash
51+
run: |
52+
if [ "${{ github.event_name }}" = "schedule" ]; then
53+
branch="main"
54+
else
55+
branch="${{ github.ref_name }}"
56+
fi
57+
58+
case "$branch" in
59+
main|next) ;;
60+
*)
61+
echo "Unsupported release branch: $branch"
62+
echo "Allowed branches: main, next"
63+
exit 1
64+
;;
65+
esac
66+
67+
echo "branch=$branch" >> "$GITHUB_OUTPUT"
68+
echo "Releasing from branch: $branch"
69+
70+
- name: Checkout code
71+
uses: actions/checkout@v6
72+
with:
73+
fetch-depth: 0
74+
ref: ${{ steps.target.outputs.branch }}
75+
76+
- name: Setup Node.js
77+
uses: actions/setup-node@v6
78+
with:
79+
node-version: 22
80+
cache: npm
81+
registry-url: https://registry.npmjs.org
82+
83+
- name: Install dependencies
84+
run: npm ci
85+
86+
- name: Build
87+
run: npm run build
88+
89+
- name: Unit tests
90+
run: npm test
91+
92+
- name: Lint and format check
93+
run: npm run check:ci
94+
95+
- name: Type check
96+
run: npx tsc --noEmit
97+
98+
- name: Run semantic-release
99+
env:
100+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
101+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
102+
GITHUB_REF: refs/heads/${{ steps.target.outputs.branch }}
103+
GITHUB_REF_NAME: ${{ steps.target.outputs.branch }}
104+
run: npm run release:run

0 commit comments

Comments
 (0)