mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-10 00:33:28 +08:00
- 将各平台驱动拆分为独立 ZIP,并为 DuckDB 同包携带运行库 - 仅生成 ZIP 下载候选,按 Gatewaysentry 与 GitHub 短测速结果调整优先级 - 扩展发布索引的归档摘要与条目元数据,强化跨平台资产校验 - 禁止裸驱动与 CI 总包进入 VPS 镜像,并校验载荷摘要和索引一致性 - 显式控制驱动镜像发布,自动清理失败暂存、过期目录和旧版本
340 lines
14 KiB
YAML
340 lines
14 KiB
YAML
name: Publish Draft Release
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- 'publish/v*'
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: Release tag to publish (for example, v0.8.7)
|
|
required: true
|
|
type: string
|
|
|
|
permissions:
|
|
actions: read
|
|
contents: write
|
|
|
|
concurrency:
|
|
# Stable and dev publications share the same mirror targets.
|
|
group: gonavi-release-publication
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
publish:
|
|
name: Validate, mirror, and publish release
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
|
|
steps:
|
|
- name: Validate release
|
|
id: validate
|
|
uses: actions/github-script@v8
|
|
with:
|
|
script: |
|
|
const requestedTag = String(process.env.RELEASE_TAG || '').trim();
|
|
const tag = requestedTag.startsWith('publish/')
|
|
? requestedTag.slice('publish/'.length)
|
|
: requestedTag;
|
|
if (!/^v\d+\.\d+\.\d+$/.test(tag)) {
|
|
core.setFailed(`Invalid release tag: ${tag}`);
|
|
return;
|
|
}
|
|
|
|
const { owner, repo } = context.repo;
|
|
const tagRef = await github.rest.git.getRef({
|
|
owner,
|
|
repo,
|
|
ref: `tags/${tag}`,
|
|
});
|
|
|
|
let commitSha = tagRef.data.object.sha;
|
|
if (tagRef.data.object.type === 'tag') {
|
|
const annotatedTag = await github.rest.git.getTag({
|
|
owner,
|
|
repo,
|
|
tag_sha: tagRef.data.object.sha,
|
|
});
|
|
commitSha = annotatedTag.data.object.sha;
|
|
}
|
|
|
|
const workflowRuns = await github.rest.actions.listWorkflowRuns({
|
|
owner,
|
|
repo,
|
|
workflow_id: 'release.yml',
|
|
event: 'push',
|
|
branch: tag,
|
|
per_page: 100,
|
|
});
|
|
const runs = workflowRuns.data.workflow_runs;
|
|
const successfulRun = runs.find((run) => (
|
|
run.head_sha === commitSha
|
|
&& run.status === 'completed'
|
|
&& run.conclusion === 'success'
|
|
));
|
|
if (!successfulRun) {
|
|
core.setFailed(`No successful Release workflow found for ${tag} at ${commitSha}`);
|
|
return;
|
|
}
|
|
|
|
const releaseList = await github.rest.repos.listReleases({
|
|
owner,
|
|
repo,
|
|
per_page: 100,
|
|
});
|
|
const releases = releaseList.data;
|
|
const release = releases.find((item) => item.tag_name === tag);
|
|
if (!release) {
|
|
core.setFailed(`Release not found for ${tag}`);
|
|
return;
|
|
}
|
|
|
|
let latestRelease = null;
|
|
try {
|
|
latestRelease = (await github.rest.repos.getLatestRelease({ owner, repo })).data;
|
|
} catch (error) {
|
|
if (error.status !== 404) {
|
|
throw error;
|
|
}
|
|
}
|
|
const semverParts = (value) => value.slice(1).split('.').map(Number);
|
|
const compareSemver = (left, right) => {
|
|
const a = semverParts(left);
|
|
const b = semverParts(right);
|
|
for (let index = 0; index < 3; index += 1) {
|
|
if (a[index] !== b[index]) {
|
|
return a[index] - b[index];
|
|
}
|
|
}
|
|
return 0;
|
|
};
|
|
let shouldPublish = release.draft;
|
|
if (release.draft) {
|
|
const latestTag = String(latestRelease?.tag_name || '').trim();
|
|
if (latestTag && /^v\d+\.\d+\.\d+$/.test(latestTag) && compareSemver(tag, latestTag) <= 0) {
|
|
core.setFailed(`Refusing to publish ${tag} over current latest ${latestTag}`);
|
|
return;
|
|
}
|
|
} else if (!latestRelease || latestRelease.id !== release.id) {
|
|
core.setFailed(`Published release ${tag} is not the current GitHub latest release`);
|
|
return;
|
|
} else {
|
|
shouldPublish = false;
|
|
}
|
|
|
|
const version = tag.slice(1);
|
|
const expectedAssets = [
|
|
'LICENSE',
|
|
'NOTICE',
|
|
'SHA256SUMS',
|
|
'latest.json',
|
|
`GoNavi-${version}-Linux-Amd64-WebKit41.tar.gz`,
|
|
`GoNavi-${version}-Linux-Amd64.tar.gz`,
|
|
`GoNavi-${version}-Linux-Arm64.tar.gz`,
|
|
`GoNavi-${version}-MacOS-Amd64.dmg`,
|
|
`GoNavi-${version}-MacOS-Arm64.dmg`,
|
|
`GoNavi-${version}-Windows-Amd64-Installer.msi`,
|
|
`GoNavi-${version}-Windows-Amd64-Portable.exe`,
|
|
`GoNavi-${version}-Windows-Amd64-Portable.zip`,
|
|
`GoNavi-${version}-Windows-Arm64-Installer.msi`,
|
|
`GoNavi-${version}-Windows-Arm64-Portable.exe`,
|
|
`GoNavi-${version}-Windows-Arm64-Portable.zip`,
|
|
];
|
|
const assetsByName = new Map(release.assets.map((asset) => [asset.name, asset]));
|
|
const missingAssets = expectedAssets.filter((name) => {
|
|
const asset = assetsByName.get(name);
|
|
return !asset || asset.state !== 'uploaded' || asset.size <= 0;
|
|
});
|
|
if (missingAssets.length > 0) {
|
|
core.setFailed(`Release assets are incomplete: ${missingAssets.join(', ')}`);
|
|
return;
|
|
}
|
|
|
|
core.setOutput('tag', tag);
|
|
core.setOutput('release_id', String(release.id));
|
|
core.setOutput('should_publish', String(shouldPublish));
|
|
core.notice(`Validated ${release.draft ? 'draft' : 'published'} release ${tag} and its assets`);
|
|
|
|
- name: Checkout release tooling
|
|
uses: actions/checkout@v5
|
|
|
|
- name: Prepare and verify stable mirror payload
|
|
id: mirror_payload
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
DRIVER_RELEASE_TOKEN: ${{ secrets.DRIVER_RELEASE_TOKEN }}
|
|
RELEASE_TAG: ${{ steps.validate.outputs.tag }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
if [[ -z "${DRIVER_RELEASE_TOKEN}" ]]; then
|
|
echo "DriverAgents release credentials are not configured" >&2
|
|
exit 1
|
|
fi
|
|
|
|
app_dir="${RUNNER_TEMP}/gonavi-release"
|
|
driver_dir="${RUNNER_TEMP}/driver-release"
|
|
mkdir -p "${app_dir}" "${driver_dir}"
|
|
gh release download "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --dir "${app_dir}"
|
|
|
|
driver_release_json="${RUNNER_TEMP}/driver-release.json"
|
|
driver_status="$(curl --silent --show-error \
|
|
--output "${driver_release_json}" \
|
|
--write-out '%{http_code}' \
|
|
--header "Authorization: Bearer ${DRIVER_RELEASE_TOKEN}" \
|
|
--header 'Accept: application/vnd.github+json' \
|
|
--header 'X-GitHub-Api-Version: 2022-11-28' \
|
|
"https://api.github.com/repos/Syngnat/GoNavi-DriverAgents/releases/tags/${RELEASE_TAG}")"
|
|
has_driver_release=false
|
|
case "${driver_status}" in
|
|
200)
|
|
jq -e --arg tag "${RELEASE_TAG}" \
|
|
'.tag_name == $tag and .draft == false' "${driver_release_json}" >/dev/null
|
|
GH_TOKEN="${DRIVER_RELEASE_TOKEN}" gh release download "${RELEASE_TAG}" \
|
|
--repo Syngnat/GoNavi-DriverAgents --dir "${driver_dir}"
|
|
has_driver_release=true
|
|
;;
|
|
404)
|
|
echo "No DriverAgents release for ${RELEASE_TAG}; skipping driver mirror"
|
|
;;
|
|
*)
|
|
echo "DriverAgents release lookup failed with HTTP ${driver_status}" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
echo "has_driver_release=${has_driver_release}" >> "$GITHUB_OUTPUT"
|
|
|
|
jq -e \
|
|
--arg tag "${RELEASE_TAG}" \
|
|
--arg mirror_base "https://download.syngnat.top/gonavi/releases/download" \
|
|
--arg github_base "https://github.com/${GITHUB_REPOSITORY}/releases/download" \
|
|
'
|
|
.tagName == $tag
|
|
and (.assets | type == "array" and length > 0)
|
|
and all(.assets[];
|
|
(.url | type == "string" and startswith($mirror_base + "/" + $tag + "/"))
|
|
and (.apiUrl | type == "string" and startswith($github_base + "/" + $tag + "/"))
|
|
and (.sha256 | type == "string" and test("^[0-9a-fA-F]{64}$"))
|
|
)
|
|
' "${app_dir}/latest.json" >/dev/null
|
|
if [[ "${has_driver_release}" == true ]]; then
|
|
jq -e '.assets | type == "object" and length > 0' \
|
|
"${driver_dir}/GoNavi-DriverAgents-Index.json" >/dev/null
|
|
[[ -s "${driver_dir}/SHA256SUMS" ]] || {
|
|
echo "DriverAgents SHA256SUMS is missing" >&2
|
|
exit 1
|
|
}
|
|
(cd "${driver_dir}" && sha256sum --check SHA256SUMS)
|
|
jq --arg tag "${RELEASE_TAG}" \
|
|
'.tagName = $tag' \
|
|
"${driver_dir}/GoNavi-DriverAgents-Index.json" \
|
|
> "${RUNNER_TEMP}/driver-latest-index.json"
|
|
fi
|
|
|
|
while IFS=$'\t' read -r name expected_size expected_sha; do
|
|
[[ "${name}" == "$(basename "${name}")" ]] || {
|
|
echo "Invalid app asset name in latest.json: ${name}" >&2
|
|
exit 1
|
|
}
|
|
file="${app_dir}/${name}"
|
|
[[ -f "${file}" ]] || {
|
|
echo "App asset referenced by latest.json is missing: ${name}" >&2
|
|
exit 1
|
|
}
|
|
actual_size="$(stat -c '%s' "${file}")"
|
|
actual_sha="$(sha256sum "${file}" | awk '{print $1}')"
|
|
if [[ "${actual_size}" != "${expected_size}" || "${actual_sha}" != "${expected_sha,,}" ]]; then
|
|
echo "App asset does not match latest.json: ${name}" >&2
|
|
exit 1
|
|
fi
|
|
done < <(jq -r '.assets[] | [.name, (.size | tostring), .sha256] | @tsv' "${app_dir}/latest.json")
|
|
|
|
if [[ "${has_driver_release}" == true ]]; then
|
|
while IFS=$'\t' read -r name expected_size; do
|
|
[[ "${name}" == "$(basename "${name}")" ]] || {
|
|
echo "Invalid driver asset name in index: ${name}" >&2
|
|
exit 1
|
|
}
|
|
file="${driver_dir}/${name}"
|
|
[[ -f "${file}" ]] || {
|
|
echo "Driver asset referenced by index is missing: ${name}" >&2
|
|
exit 1
|
|
}
|
|
actual_size="$(stat -c '%s' "${file}")"
|
|
if [[ "${actual_size}" != "${expected_size}" ]]; then
|
|
echo "Driver asset size does not match index: ${name}" >&2
|
|
exit 1
|
|
fi
|
|
done < <(jq -r '.assets | to_entries[] | [.key, (.value | tostring)] | @tsv' \
|
|
"${driver_dir}/GoNavi-DriverAgents-Index.json")
|
|
fi
|
|
|
|
- name: Publish validated draft
|
|
if: steps.validate.outputs.should_publish == 'true'
|
|
uses: actions/github-script@v8
|
|
env:
|
|
RELEASE_TAG: ${{ steps.validate.outputs.tag }}
|
|
RELEASE_ID: ${{ steps.validate.outputs.release_id }}
|
|
with:
|
|
script: |
|
|
const tag = String(process.env.RELEASE_TAG || '').trim();
|
|
const releaseId = Number(process.env.RELEASE_ID || 0);
|
|
if (!/^v\d+\.\d+\.\d+$/.test(tag) || !Number.isSafeInteger(releaseId) || releaseId <= 0) {
|
|
core.setFailed(`Invalid validated release: tag=${tag} id=${releaseId}`);
|
|
return;
|
|
}
|
|
const { owner, repo } = context.repo;
|
|
const published = await github.rest.repos.updateRelease({
|
|
owner,
|
|
repo,
|
|
release_id: releaseId,
|
|
draft: false,
|
|
prerelease: false,
|
|
make_latest: 'true',
|
|
});
|
|
if (published.data.draft || !published.data.published_at) {
|
|
core.setFailed(`Release ${tag} was not published successfully`);
|
|
return;
|
|
}
|
|
core.notice(`Published ${tag}: ${published.data.html_url}`);
|
|
|
|
- name: Verify GitHub release is published and latest
|
|
uses: actions/github-script@v8
|
|
env:
|
|
RELEASE_TAG: ${{ steps.validate.outputs.tag }}
|
|
RELEASE_ID: ${{ steps.validate.outputs.release_id }}
|
|
with:
|
|
script: |
|
|
const tag = String(process.env.RELEASE_TAG || '').trim();
|
|
const releaseId = Number(process.env.RELEASE_ID || 0);
|
|
const { owner, repo } = context.repo;
|
|
const [releaseResult, latestResult] = await Promise.all([
|
|
github.rest.repos.getRelease({ owner, repo, release_id: releaseId }),
|
|
github.rest.repos.getLatestRelease({ owner, repo }),
|
|
]);
|
|
const release = releaseResult.data;
|
|
const latest = latestResult.data;
|
|
if (release.draft || !release.published_at || release.tag_name !== tag || latest.id !== release.id) {
|
|
core.setFailed(`Release ${tag} is not the current published GitHub latest release`);
|
|
return;
|
|
}
|
|
core.notice(`Verified GitHub latest release ${tag}`);
|
|
|
|
- name: Mirror stable release to Gatewaysentry
|
|
uses: ./.github/actions/publish-vps-mirror
|
|
with:
|
|
channel: stable
|
|
app-tag: ${{ steps.validate.outputs.tag }}
|
|
app-dir: ${{ runner.temp }}/gonavi-release
|
|
app-manifest: ${{ runner.temp }}/gonavi-release/latest.json
|
|
driver-enabled: ${{ steps.mirror_payload.outputs.has_driver_release }}
|
|
driver-tag: ${{ steps.validate.outputs.tag }}
|
|
driver-dir: ${{ runner.temp }}/driver-release
|
|
driver-version-index: ${{ runner.temp }}/driver-release/GoNavi-DriverAgents-Index.json
|
|
driver-latest-index: ${{ runner.temp }}/driver-latest-index.json
|
|
ssh-host: ${{ secrets.MIRROR_SSH_HOST }}
|
|
ssh-port: ${{ secrets.MIRROR_SSH_PORT }}
|
|
ssh-user: ${{ secrets.MIRROR_SSH_USER }}
|
|
ssh-private-key: ${{ secrets.MIRROR_SSH_PRIVATE_KEY }}
|
|
ssh-known-hosts: ${{ secrets.MIRROR_SSH_KNOWN_HOSTS }}
|