feat: 支持按 Tag 打包,分支管理可添加 Tag
- 分支管理新增「分支 / Tag」类型选择,Tag 存于 config["tags"],与分支不可重名 - 每个 Tag 与分支一样拥有同名独立版本轨道,默认上架轨 - 打包页按「分支」「Tag」分组选择;任务新增 ref_type 字段记录引用类型 - Tag 打包时 fetch --tags --force 后以游离 HEAD 检出 refs/tags/<tag> - 历史记录与下载页按类型显示「分支」或「Tag」 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -73,6 +73,46 @@ describe('BuildView.vue', () => {
|
||||
expect(branchSelect).toBeTruthy()
|
||||
})
|
||||
|
||||
it('分支和 Tag 分组显示,可选择 Tag 提交', async () => {
|
||||
fetch.mockImplementation((url, opts) => {
|
||||
if (url === '/api/tasks' && opts?.method === 'POST') {
|
||||
return Promise.resolve({ ok: true, json: () => Promise.resolve({ id: 'tag-task', status: 'pending' }) })
|
||||
}
|
||||
const responses = {
|
||||
'/api/apps': { '1': { name: '测试App', server: '测试环境', certificates: { Ad_Hoc: {} } } },
|
||||
'/api/schemes': { '1': { name: 'testScheme' } },
|
||||
'/api/branches': ['main', 'dev'],
|
||||
'/api/tags': ['v3.2.0'],
|
||||
'/api/tasks': [],
|
||||
}
|
||||
return Promise.resolve({ json: () => Promise.resolve(responses[url] || {}) })
|
||||
})
|
||||
|
||||
const wrapper = mountBuildView()
|
||||
await flushPromises()
|
||||
|
||||
const groups = wrapper.findAll('optgroup')
|
||||
expect(groups.map(g => g.attributes('label'))).toEqual(['分支', 'Tag'])
|
||||
expect(groups[1].findAll('option').map(o => o.text())).toEqual(['v3.2.0'])
|
||||
|
||||
wrapper.vm.selectedServer = '测试环境'
|
||||
await wrapper.vm.$nextTick()
|
||||
wrapper.vm.form.app_id = '1'
|
||||
wrapper.vm.form.branch = 'v3.2.0'
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.find('.btn-primary').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
const post = fetch.mock.calls.find(([url, opts]) => url === '/api/tasks' && opts?.method === 'POST')
|
||||
expect(JSON.parse(post[1].body).branch).toBe('v3.2.0')
|
||||
})
|
||||
|
||||
it('没有 Tag 时不显示 Tag 分组', async () => {
|
||||
const wrapper = mountBuildView()
|
||||
await flushPromises()
|
||||
expect(wrapper.findAll('optgroup').map(g => g.attributes('label'))).toEqual(['分支'])
|
||||
})
|
||||
|
||||
it('默认值正确', async () => {
|
||||
const wrapper = mountBuildView()
|
||||
await flushPromises()
|
||||
|
||||
@@ -23,6 +23,7 @@ function mockFetch(url) {
|
||||
'/api/config/schemes': { '1': { name: 'testScheme', ossFloder: 'test' } },
|
||||
'/api/config/servers': { '测试环境': { api: 'https://test.com', assDom: '', universalLink: '' } },
|
||||
'/api/config/branches': ['main', 'dev'],
|
||||
'/api/config/tags': ['v3.2.0'],
|
||||
'/api/config/build': { max_concurrent_builds: 2, build_dir_retention_hours: 24, build_base_dir: '/tmp' },
|
||||
'/api/config/versions': {
|
||||
app_ver: '2.195.0',
|
||||
@@ -31,8 +32,9 @@ function mockFetch(url) {
|
||||
tracks: {
|
||||
release: { kind: 'release', app_ver: '2.195.0', build_ver: '2.195.0.0', build_map: {} },
|
||||
dev: { kind: 'feature', app_ver: '2.100.0', build_ver: '2.100.0.0', build_map: {} },
|
||||
'v3.2.0': { kind: 'release', app_ver: '3.2.0', build_ver: '3.2.0.0', build_map: {} },
|
||||
},
|
||||
branch_track: { main: 'release', dev: 'dev' },
|
||||
branch_track: { main: 'release', dev: 'dev', 'v3.2.0': 'v3.2.0' },
|
||||
},
|
||||
'/api/config': { apps: {}, schemes: {}, branches: ['main'] },
|
||||
}
|
||||
@@ -253,4 +255,41 @@ describe('ConfigView.vue', () => {
|
||||
|
||||
expect(calls).toEqual([{ branch: 'main', track: 'feature' }])
|
||||
})
|
||||
|
||||
it('分支管理可添加和删除 Tag', async () => {
|
||||
window.confirm = vi.fn(() => true)
|
||||
const calls = []
|
||||
fetch.mockImplementation((url, opts) => {
|
||||
if (opts?.method === 'POST' || opts?.method === 'DELETE') {
|
||||
calls.push([opts.method, url, opts.body && JSON.parse(opts.body)])
|
||||
return Promise.resolve({ ok: true, json: () => Promise.resolve({ message: 'ok' }) })
|
||||
}
|
||||
return mockFetch(url)
|
||||
})
|
||||
|
||||
const wrapper = mountConfigView()
|
||||
await flushPromises()
|
||||
|
||||
const branchesMenu = wrapper.findAll('.sidebar-menu li').find(li => li.text() === '分支管理')
|
||||
await branchesMenu.trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const rows = wrapper.findAll('.config-table tbody tr')
|
||||
expect(rows.map(r => r.findAll('td').slice(0, 2).map(td => td.text()))).toEqual([
|
||||
['main', '分支'], ['dev', '分支'], ['v3.2.0', 'Tag'],
|
||||
])
|
||||
|
||||
await wrapper.find('select').setValue('tag')
|
||||
await wrapper.find('input[placeholder="输入 Tag 名称"]').setValue('v3.3.0')
|
||||
await wrapper.findAll('.btn-primary').find(b => b.text() === '添加 Tag').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
await rows[2].find('.action-btn.delete').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(calls).toEqual([
|
||||
['POST', '/api/config/tags', { name: 'v3.3.0' }],
|
||||
['DELETE', '/api/config/tags/v3.2.0', undefined],
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -41,9 +41,14 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>代码分支</label>
|
||||
<label>代码分支 / Tag</label>
|
||||
<select v-model="form.branch">
|
||||
<option v-for="b in branches" :key="b" :value="b">{{ b }}</option>
|
||||
<optgroup label="分支">
|
||||
<option v-for="b in branches" :key="b" :value="b">{{ b }}</option>
|
||||
</optgroup>
|
||||
<optgroup v-if="tags.length" label="Tag">
|
||||
<option v-for="t in tags" :key="`tag:${t}`" :value="t">{{ t }}</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -167,6 +172,7 @@ const authFetch = (url, options = {}) => {
|
||||
const apps = ref({})
|
||||
const schemes = ref({})
|
||||
const branches = ref(['main'])
|
||||
const tags = ref([])
|
||||
const tasks = ref([])
|
||||
const selectedServer = ref('')
|
||||
const serverEnvironments = computed(() => {
|
||||
@@ -314,15 +320,18 @@ onUnmounted(() => {
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const [appsRes, schemesRes, branchesRes, tasksRes] = await Promise.all([
|
||||
const [appsRes, schemesRes, branchesRes, tagsRes, tasksRes] = await Promise.all([
|
||||
authFetch('/api/apps'),
|
||||
authFetch('/api/schemes'),
|
||||
authFetch('/api/branches'),
|
||||
authFetch('/api/tags'),
|
||||
authFetch('/api/tasks'),
|
||||
])
|
||||
apps.value = await appsRes.json()
|
||||
schemes.value = await schemesRes.json()
|
||||
branches.value = await branchesRes.json()
|
||||
const tagList = await tagsRes.json()
|
||||
tags.value = Array.isArray(tagList) ? tagList : []
|
||||
const allTasks = await tasksRes.json()
|
||||
|
||||
// 从历史页跳转过来时,读取 taskId 查询参数
|
||||
@@ -436,7 +445,7 @@ const errorCategoryHint = (cat) => {
|
||||
provisioning: '描述文件问题:描述文件过期、不匹配或未安装',
|
||||
compilation: '代码编译错误:请检查代码语法和类型引用',
|
||||
dependency: '依赖问题:Pod 依赖缺失或版本不兼容',
|
||||
git: '代码拉取失败:请检查分支是否存在',
|
||||
git: '代码拉取失败:请检查分支或 Tag 是否存在',
|
||||
config: '配置替换失败:请检查打包配置',
|
||||
build: '构建配置错误:请检查 Scheme 和 Workspace 配置',
|
||||
unknown: '打包失败:请查看详细日志排查原因',
|
||||
|
||||
@@ -152,36 +152,42 @@
|
||||
<div v-if="tab === 'branches'">
|
||||
<h2>分支管理</h2>
|
||||
<p style="color: #666; margin-bottom: 16px; font-size: 14px;">
|
||||
管理可打包的代码分支,打包时从对应分支目录获取源码。
|
||||
版本轨道决定该分支使用哪一套版本号:上架(master/develop 共用,App_Store 打包构建号自增)、自测(feature 用,构建号不自增)。
|
||||
管理可打包的代码分支和 Tag,打包时从对应分支或 Tag 获取源码。
|
||||
每个分支 / Tag 各有一套独立的版本号,版本轨道决定构建号行为:上架(App_Store 打包构建号自增)、自测(构建号不自增)。新增 Tag 默认为上架。
|
||||
</p>
|
||||
<div style="display: flex; gap: 12px; margin-bottom: 16px;">
|
||||
<input v-model="newBranch" type="text" placeholder="输入分支名称" style="flex: 1; padding: 10px 12px; border: 1px solid #d9d9d9; border-radius: 6px; font-size: 14px;">
|
||||
<button class="btn btn-primary" style="width: auto; padding: 10px 24px;" @click="addBranch">添加分支</button>
|
||||
<select v-model="newRefType" style="width: auto; padding: 10px 12px;">
|
||||
<option value="branch">分支</option>
|
||||
<option value="tag">Tag</option>
|
||||
</select>
|
||||
<input v-model="newBranch" type="text" :placeholder="newRefType === 'tag' ? '输入 Tag 名称' : '输入分支名称'" style="flex: 1; padding: 10px 12px; border: 1px solid #d9d9d9; border-radius: 6px; font-size: 14px;">
|
||||
<button class="btn btn-primary" style="width: auto; padding: 10px 24px;" @click="addBranch">{{ newRefType === 'tag' ? '添加 Tag' : '添加分支' }}</button>
|
||||
</div>
|
||||
<table class="config-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>分支名称</th>
|
||||
<th>名称</th>
|
||||
<th>类型</th>
|
||||
<th>版本轨道</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="b in branches" :key="b">
|
||||
<td><strong>{{ b }}</strong></td>
|
||||
<tr v-for="r in buildRefs" :key="`${r.type}:${r.name}`">
|
||||
<td><strong>{{ r.name }}</strong></td>
|
||||
<td>{{ r.type === 'tag' ? 'Tag' : '分支' }}</td>
|
||||
<td>
|
||||
<select :value="branchKind(b)" style="width: auto; padding: 4px 8px;" @change="setBranchTrack(b, $event.target.value)">
|
||||
<select :value="branchKind(r.name)" style="width: auto; padding: 4px 8px;" @change="setBranchTrack(r.name, $event.target.value)">
|
||||
<option value="release">上架(App_Store 打包构建号自增)</option>
|
||||
<option value="feature">自测(构建号不自增)</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="action-btns">
|
||||
<button class="action-btn delete" @click="deleteBranch(b)">删除</button>
|
||||
<button class="action-btn delete" @click="deleteBranch(r)">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!branches.length">
|
||||
<td colspan="3" style="text-align: center; color: #999;">暂无分支配置</td>
|
||||
<tr v-if="!buildRefs.length">
|
||||
<td colspan="4" style="text-align: center; color: #999;">暂无分支配置</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -297,7 +303,7 @@
|
||||
<label>App_Ver(应用版本)</label>
|
||||
<input type="text" v-model="versions[t.name].app_ver" placeholder="2.180.0" @input="onAppVerInput(t.name)">
|
||||
<div style="font-size: 12px; color: #999; margin-top: 4px;">
|
||||
格式:主版本.次版本.修订号。该版本号只用于 {{ t.branches.join(' / ') }} 分支(轨道 {{ t.name }}),与其它分支互不影响。
|
||||
格式:主版本.次版本.修订号。该版本号只用于 {{ t.branches.join(' / ') }}(轨道 {{ t.name }}),与其它分支 / Tag 互不影响。
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -647,7 +653,14 @@ const apps = ref({})
|
||||
const schemes = ref({})
|
||||
const servers = ref({})
|
||||
const branches = ref([])
|
||||
const tags = ref([])
|
||||
const newBranch = ref('')
|
||||
const newRefType = ref('branch') // 新增的是分支还是 Tag
|
||||
// 分支管理表格:分支在前,Tag 在后
|
||||
const buildRefs = computed(() => [
|
||||
...branches.value.map(name => ({ name, type: 'branch' })),
|
||||
...tags.value.map(name => ({ name, type: 'tag' })),
|
||||
])
|
||||
const buildSettings = ref({})
|
||||
// 版本轨道:每个分支一条独立轨道,轨道名即分支名(master/main 沿用历史名 release)。
|
||||
// 轨道类型 kind 决定构建号行为:release=上架、App_Store 打包自增;feature=自测、不自增。
|
||||
@@ -678,13 +691,14 @@ const branchKind = (branch) => trackKind.value[branchTrack.value[branch]] || 'fe
|
||||
// 打包设置里按分支分块展示版本号;多个分支共用一条轨道时只显示一块
|
||||
const branchTracks = computed(() => {
|
||||
const seen = new Set()
|
||||
return branches.value
|
||||
const refs = [...branches.value, ...tags.value]
|
||||
return refs
|
||||
.map(b => branchTrack.value[b])
|
||||
.filter(name => name && versions.value[name] && !seen.has(name) && seen.add(name))
|
||||
.map(name => ({
|
||||
name,
|
||||
kind: trackKind.value[name] || 'release',
|
||||
branches: branches.value.filter(b => branchTrack.value[b] === name),
|
||||
branches: refs.filter(b => branchTrack.value[b] === name),
|
||||
}))
|
||||
})
|
||||
|
||||
@@ -747,11 +761,12 @@ const loadData = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
const [appsRes, schemesRes, serversRes, branchesRes, buildRes, uploadRes, configRes, versionsRes, usersRes] = await Promise.all([
|
||||
const [appsRes, schemesRes, serversRes, branchesRes, tagsRes, buildRes, uploadRes, configRes, versionsRes, usersRes] = await Promise.all([
|
||||
authFetch('/api/config/apps'),
|
||||
authFetch('/api/config/schemes'),
|
||||
authFetch('/api/config/servers'),
|
||||
authFetch('/api/config/branches'),
|
||||
authFetch('/api/config/tags'),
|
||||
authFetch('/api/config/build'),
|
||||
authFetch('/api/config/upload'),
|
||||
authFetch('/api/config'),
|
||||
@@ -762,6 +777,8 @@ const loadData = async () => {
|
||||
schemes.value = await schemesRes.json()
|
||||
servers.value = await serversRes.json()
|
||||
branches.value = await branchesRes.json()
|
||||
const tagList = await tagsRes.json()
|
||||
tags.value = Array.isArray(tagList) ? tagList : []
|
||||
buildSettings.value = await buildRes.json()
|
||||
uploadConfig.value = await uploadRes.json()
|
||||
jsonContent.value = JSON.stringify(await configRes.json(), null, 2)
|
||||
@@ -1178,12 +1195,12 @@ const deleteScheme = async (id) => {
|
||||
await loadData()
|
||||
}
|
||||
|
||||
// 分支管理
|
||||
// 分支管理(分支与 Tag 共用一张表)
|
||||
const addBranch = async () => {
|
||||
const name = newBranch.value.trim()
|
||||
if (!name) return
|
||||
try {
|
||||
const res = await authFetch('/api/config/branches', {
|
||||
const res = await authFetch(newRefType.value === 'tag' ? '/api/config/tags' : '/api/config/branches', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name }),
|
||||
@@ -1218,10 +1235,12 @@ const setBranchTrack = async (name, track) => {
|
||||
}
|
||||
}
|
||||
|
||||
const deleteBranch = async (name) => {
|
||||
if (!confirm(`确定删除分支「${name}」?`)) return
|
||||
const deleteBranch = async ({ name, type }) => {
|
||||
const isTag = type === 'tag'
|
||||
if (!confirm(`确定删除${isTag ? ' Tag ' : '分支'}「${name}」?`)) return
|
||||
try {
|
||||
const res = await authFetch(`/api/config/branches/${encodeURIComponent(name)}`, { method: 'DELETE' })
|
||||
const base = isTag ? '/api/config/tags' : '/api/config/branches'
|
||||
const res = await authFetch(`${base}/${encodeURIComponent(name)}`, { method: 'DELETE' })
|
||||
if (!res.ok) throw new Error('删除失败')
|
||||
await loadData()
|
||||
} catch (e) {
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
<span :class="['build-type-badge', logTask?.build_type === 'App_Store' ? 'badge-appstore' : 'badge-adhoc']">{{ logTask?.build_type }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">分支</span>
|
||||
<span class="meta-label">{{ logTask?.ref_type === 'tag' ? 'Tag' : '分支' }}</span>
|
||||
<span class="meta-value">{{ logTask?.branch || '-' }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
|
||||
Reference in New Issue
Block a user