* feat: add stable and beta Homebrew channels * fix: align beta formula with tap conventions * fix: use dedicated token for Homebrew PRs * chore: minimize release token permissions * docs: record Homebrew token setup * docs: keep Homebrew automation token long-lived * fix(verify): assert channel versions and prove homebrew coexistence The six-channel verifier previously ran `dws version` without comparing it to the version each channel advertises, so a stale or wrong binary still reported PASS. It also uninstalled stable before installing beta, which could not prove the keg-only beta coexists with stable. - smoke() now takes an expected version and fails the channel on mismatch - npm/homebrew derive the expected version from the package manager; curl/upgrade derive it from the latest GitHub release tag - homebrew installs keg-only beta while stable stays installed, then asserts stable's version, binary SHA and PATH link are unchanged - cleanup uninstalls both script-installed formulae, still refusing to touch a pre-existing user install - add regression tests for version assertion and coexistence semantics * style(formula): satisfy brew style for stable and beta formulae - reword desc so it no longer starts with the formula name - drop the unnecessary `require "fileutils"` and use the mixed-in cp_r instead of the FileUtils. qualifier * fix(verify): compare channel versions exactly * fix(homebrew): keep generated formulae style-clean * fix(homebrew): sync formulae with latest releases
327 lines
13 KiB
YAML
327 lines
13 KiB
YAML
name: Release
|
||
|
||
on:
|
||
push:
|
||
tags:
|
||
- "v*"
|
||
workflow_dispatch:
|
||
inputs:
|
||
repair_npm_version:
|
||
description: "Only publish an existing release to npm, e.g. v1.0.48"
|
||
required: false
|
||
type: string
|
||
|
||
permissions:
|
||
contents: write
|
||
|
||
jobs:
|
||
release:
|
||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.repair_npm_version == '' }}
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 30
|
||
|
||
steps:
|
||
- name: Check out repository
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
|
||
- name: Check Homebrew PR automation token
|
||
if: ${{ github.repository_owner == 'DingTalk-Real-AI' }}
|
||
# Organization policy intentionally prevents the broad, built-in
|
||
# GITHUB_TOKEN from creating PRs. Keep Formula automation on a
|
||
# repository-scoped token instead of weakening that policy.
|
||
env:
|
||
HOMEBREW_PR_TOKEN: ${{ secrets.HOMEBREW_PR_TOKEN }}
|
||
run: |
|
||
if [ -z "${HOMEBREW_PR_TOKEN:-}" ]; then
|
||
echo "HOMEBREW_PR_TOKEN is required to open Formula PRs from official releases" >&2
|
||
exit 1
|
||
fi
|
||
|
||
- name: Set up Go
|
||
uses: actions/setup-go@v5
|
||
with:
|
||
go-version-file: go.mod
|
||
|
||
- name: Install archive tooling
|
||
run: sudo apt-get update && sudo apt-get install -y zip unzip
|
||
|
||
- name: Multi Profile E2E
|
||
run: bash scripts/dev/test-multi-profile-e2e.sh
|
||
|
||
- name: Install rcodesign (sign darwin binaries from Linux)
|
||
run: |
|
||
set -euo pipefail
|
||
RCS_VERSION="0.29.0"
|
||
RCS_ARCHIVE_SHA256="dbe85cedd8ee4217b64e9a0e4c2aef92ab8bcaaa41f20bde99781ff02e600002"
|
||
curl -fsSL -o /tmp/rcodesign.tar.gz \
|
||
"https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F${RCS_VERSION}/apple-codesign-${RCS_VERSION}-x86_64-unknown-linux-musl.tar.gz"
|
||
printf '%s %s\n' "$RCS_ARCHIVE_SHA256" /tmp/rcodesign.tar.gz \
|
||
| sha256sum --check --strict -
|
||
mkdir -p /tmp/rcodesign
|
||
tar -xzf /tmp/rcodesign.tar.gz -C /tmp/rcodesign --strip-components=1
|
||
sudo install -m 0755 /tmp/rcodesign/rcodesign /usr/local/bin/rcodesign
|
||
rcodesign --version
|
||
|
||
- name: Prepare Apple Developer ID certificate
|
||
env:
|
||
APPLE_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_CERTIFICATE_P12_BASE64 }}
|
||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||
run: |
|
||
set -euo pipefail
|
||
|
||
if [ -z "${APPLE_CERTIFICATE_P12_BASE64:-}" ] || [ -z "${APPLE_CERTIFICATE_PASSWORD:-}" ]; then
|
||
if [ "$GITHUB_REPOSITORY_OWNER" = "DingTalk-Real-AI" ]; then
|
||
echo "APPLE_CERTIFICATE_P12_BASE64 and APPLE_CERTIFICATE_PASSWORD are required for official releases" >&2
|
||
exit 1
|
||
fi
|
||
echo "Developer ID secrets are unavailable; fork release will use ad-hoc signing."
|
||
exit 0
|
||
fi
|
||
|
||
umask 077
|
||
certificate_path="$RUNNER_TEMP/dws-developer-id.p12"
|
||
password_path="$RUNNER_TEMP/dws-developer-id-password"
|
||
printf '%s' "$APPLE_CERTIFICATE_P12_BASE64" | base64 --decode > "$certificate_path"
|
||
printf '%s' "$APPLE_CERTIFICATE_PASSWORD" > "$password_path"
|
||
|
||
# Fail before packaging if the secret is corrupt or the password is wrong.
|
||
# The exported P12 may use legacy PKCS#12 ciphers; OpenSSL 3 requires
|
||
# -legacy to validate those containers even though rcodesign can read them.
|
||
openssl pkcs12 -legacy -in "$certificate_path" -passin "file:$password_path" -noout
|
||
|
||
echo "DWS_APPLE_CERTIFICATE_P12=$certificate_path" >> "$GITHUB_ENV"
|
||
echo "DWS_APPLE_CERTIFICATE_PASSWORD_FILE=$password_path" >> "$GITHUB_ENV"
|
||
|
||
- name: Run GoReleaser
|
||
uses: goreleaser/goreleaser-action@v6
|
||
with:
|
||
version: "~> v2"
|
||
args: release --clean
|
||
env:
|
||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: Post-release packaging
|
||
run: ./scripts/release/post-goreleaser.sh
|
||
env:
|
||
DWS_PACKAGE_VERSION: ${{ github.ref_name }}
|
||
DWS_REQUIRE_DEVELOPER_ID_SIGNING: ${{ github.repository_owner == 'DingTalk-Real-AI' }}
|
||
|
||
- name: Remove Apple Developer ID certificate
|
||
if: ${{ always() }}
|
||
run: |
|
||
rm -f "$RUNNER_TEMP/dws-developer-id.p12"
|
||
rm -f "$RUNNER_TEMP/dws-developer-id-password"
|
||
|
||
# GoReleaser uploads the original archives to a Draft before
|
||
# post-goreleaser.sh replaces the Darwin binaries. Re-upload every changed
|
||
# file, verify the Draft digests, and keep it private for Apple validation.
|
||
- name: Upload finalized signed assets to release
|
||
env:
|
||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
DWS_PUBLISH_RELEASE: "false"
|
||
run: ./scripts/release/finalize-github-release.sh
|
||
|
||
- name: Preserve finalized distribution files
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: finalized-release-dist
|
||
path: dist/
|
||
if-no-files-found: error
|
||
retention-days: 1
|
||
|
||
verify-darwin-signatures:
|
||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.repair_npm_version == '' }}
|
||
needs: release
|
||
runs-on: macos-latest
|
||
timeout-minutes: 10
|
||
|
||
steps:
|
||
- name: Download finalized Darwin assets from Draft release
|
||
env:
|
||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
run: |
|
||
set -euo pipefail
|
||
mkdir -p dist
|
||
gh release download "$GITHUB_REF_NAME" \
|
||
--repo "$GITHUB_REPOSITORY" \
|
||
--dir dist \
|
||
--pattern 'dws-darwin-amd64.tar.gz' \
|
||
--pattern 'dws-darwin-arm64.tar.gz' \
|
||
--clobber
|
||
|
||
- name: Verify finalized Darwin signatures with Apple codesign
|
||
run: |
|
||
set -euo pipefail
|
||
for arch in amd64 arm64; do
|
||
archive="dist/dws-darwin-${arch}.tar.gz"
|
||
stage="$RUNNER_TEMP/verify-darwin-${arch}"
|
||
mkdir -p "$stage"
|
||
tar -xzf "$archive" -C "$stage"
|
||
test -f "$stage/dws"
|
||
codesign --verify --strict --verbose=4 "$stage/dws"
|
||
codesign -dvvv "$stage/dws"
|
||
done
|
||
|
||
publish-release:
|
||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.repair_npm_version == '' }}
|
||
needs:
|
||
- release
|
||
- verify-darwin-signatures
|
||
runs-on: ubuntu-latest
|
||
# Mirroring every release asset to Gitee can be slow; 30 minutes previously
|
||
# cut the fallback upload off mid-run.
|
||
timeout-minutes: 60
|
||
|
||
steps:
|
||
- name: Check out repository
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Restore finalized distribution files
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
name: finalized-release-dist
|
||
path: dist
|
||
|
||
- name: Publish verified Draft release
|
||
env:
|
||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
run: gh release edit "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --draft=false
|
||
|
||
- name: Open stable Homebrew formula PR
|
||
# Beta builds must never replace the stable Homebrew formula. Formula
|
||
# updates use the normal PR path instead of writing main from a tag job.
|
||
if: ${{ github.repository_owner == 'DingTalk-Real-AI' && !contains(github.ref_name, '-') }}
|
||
run: ./scripts/release/publish-homebrew-formula.sh
|
||
env:
|
||
DWS_TAP_REPO_URL: https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git
|
||
DWS_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_PR_TOKEN }}
|
||
DWS_TAP_PR_REPOSITORY: ${{ github.repository }}
|
||
DWS_TAP_PR_BRANCH: "automation/homebrew-${{ github.ref_name }}"
|
||
DWS_TAP_PR_TITLE: "chore: update Homebrew formula for ${{ github.ref_name }}"
|
||
DWS_TAP_COMMIT_MESSAGE: "chore: update formula for ${{ github.ref_name }}"
|
||
|
||
- name: Open beta Homebrew formula PR
|
||
# Keep beta in a separately named, keg-only Formula so it cannot replace the
|
||
# stable dws link for ordinary Homebrew users.
|
||
if: ${{ github.repository_owner == 'DingTalk-Real-AI' && contains(github.ref_name, '-') }}
|
||
run: ./scripts/release/publish-homebrew-formula.sh
|
||
env:
|
||
DWS_FORMULA_SOURCE: dist/homebrew/dingtalk-workspace-cli-beta.rb
|
||
DWS_TAP_FORMULA_PATH: Formula/dingtalk-workspace-cli-beta.rb
|
||
DWS_TAP_REPO_URL: https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli.git
|
||
DWS_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_PR_TOKEN }}
|
||
DWS_TAP_PR_REPOSITORY: ${{ github.repository }}
|
||
DWS_TAP_PR_BRANCH: "automation/homebrew-beta-${{ github.ref_name }}"
|
||
DWS_TAP_PR_TITLE: "chore: update Homebrew beta formula for ${{ github.ref_name }}"
|
||
DWS_TAP_COMMIT_MESSAGE: "chore: update beta formula for ${{ github.ref_name }}"
|
||
|
||
- name: Sync release to China OSS mirror
|
||
# 自动同步到国内镜像,供 install.sh 的 DWS_RELEASE_BASE 开关消费。
|
||
# 脚本自带门控:未配置 OSS_* secret 时优雅跳过,不影响海外发布。
|
||
run: ./scripts/release/sync-to-oss.sh
|
||
env:
|
||
VERSION: ${{ github.ref_name }}
|
||
OSS_ACCESS_KEY_ID: ${{ secrets.OSS_ACCESS_KEY_ID }}
|
||
OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
|
||
OSS_ENDPOINT: ${{ secrets.OSS_ENDPOINT }}
|
||
OSS_BUCKET: ${{ secrets.OSS_BUCKET }}
|
||
OSS_PREFIX: ${{ secrets.OSS_PREFIX }}
|
||
|
||
- name: Setup Node.js
|
||
uses: actions/setup-node@v4
|
||
with:
|
||
node-version: "20"
|
||
registry-url: "https://registry.npmjs.org"
|
||
|
||
- name: Publish stable to npm
|
||
# 只有官方仓库发 npm;fork(dev 预览)没有 NPM_TOKEN,跳过以免红叉。
|
||
# 必须在 Gitee mirror 前发布:Gitee 附件上传偶发长时间挂住,不能阻塞 npm/latest。
|
||
if: ${{ github.repository_owner == 'DingTalk-Real-AI' && !contains(github.ref_name, '-') }}
|
||
working-directory: dist/npm/dingtalk-workspace-cli
|
||
run: npm publish --access public
|
||
env:
|
||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||
|
||
- name: Publish prerelease to npm beta
|
||
# 预发布版本不能更新 npm latest,避免普通 npm 安装链路拿到 beta。
|
||
if: ${{ github.repository_owner == 'DingTalk-Real-AI' && contains(github.ref_name, '-') }}
|
||
working-directory: dist/npm/dingtalk-workspace-cli
|
||
run: npm publish --access public --tag beta
|
||
env:
|
||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||
|
||
- name: Mirror release to Gitee (China)
|
||
# 把 release 附件(二进制/校验和/skills 包)镜像到 Gitee release,供 install.sh
|
||
# 的 DWS_GITEE_REPO 开关消费(仓库代码由 Gitee 仓库镜像功能自动同步,附件不在其内)。
|
||
# 默认关闭:国内 release 应由 Gitee 侧本地构建发布,避免 GitHub -> Gitee 跨境传大包卡住。
|
||
# 仅在需要临时补救时设置 repo variable ENABLE_GITEE_UPLOAD_FALLBACK=true。
|
||
if: ${{ vars.ENABLE_GITEE_UPLOAD_FALLBACK == 'true' }}
|
||
timeout-minutes: 20
|
||
run: ./scripts/release/sync-to-gitee.sh
|
||
env:
|
||
VERSION: ${{ github.ref_name }}
|
||
GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }}
|
||
GITEE_USER: ${{ secrets.GITEE_USER }}
|
||
GITEE_REPO: ${{ secrets.GITEE_REPO }}
|
||
|
||
repair-npm:
|
||
if: ${{ github.event_name == 'workflow_dispatch' && inputs.repair_npm_version != '' }}
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 15
|
||
steps:
|
||
- name: Check out repository
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Download GitHub release assets
|
||
env:
|
||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
run: |
|
||
set -eu
|
||
mkdir -p dist
|
||
gh release download "${{ inputs.repair_npm_version }}" \
|
||
--repo "${{ github.repository }}" \
|
||
--dir dist \
|
||
--pattern 'dws-*' \
|
||
--pattern 'checksums.txt' \
|
||
--clobber
|
||
ls -la dist
|
||
|
||
- name: Stage npm package
|
||
run: |
|
||
set -eu
|
||
version="${{ inputs.repair_npm_version }}"
|
||
semver="${version#v}"
|
||
pkg_root="dist/npm/dingtalk-workspace-cli"
|
||
rm -rf "$pkg_root"
|
||
mkdir -p "$pkg_root/assets" "$pkg_root/bin"
|
||
cp build/npm/install.js "$pkg_root/install.js"
|
||
cp build/npm/bin/dws.js "$pkg_root/bin/dws.js"
|
||
cp build/npm/README.md "$pkg_root/README.md"
|
||
sed "s|__VERSION__|${semver}|g" build/npm/package.json.tmpl > "$pkg_root/package.json"
|
||
cp dist/dws-* "$pkg_root/assets/"
|
||
cp dist/checksums.txt "$pkg_root/assets/"
|
||
test -f "$pkg_root/assets/dws-skills.zip"
|
||
cat "$pkg_root/package.json"
|
||
|
||
- name: Setup Node.js
|
||
uses: actions/setup-node@v4
|
||
with:
|
||
node-version: "20"
|
||
registry-url: "https://registry.npmjs.org"
|
||
|
||
- name: Publish stable to npm
|
||
if: ${{ github.repository_owner == 'DingTalk-Real-AI' && !contains(inputs.repair_npm_version, '-') }}
|
||
working-directory: dist/npm/dingtalk-workspace-cli
|
||
run: npm publish --access public
|
||
env:
|
||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||
|
||
- name: Publish prerelease to npm beta
|
||
if: ${{ github.repository_owner == 'DingTalk-Real-AI' && contains(inputs.repair_npm_version, '-') }}
|
||
working-directory: dist/npm/dingtalk-workspace-cli
|
||
run: npm publish --access public --tag beta
|
||
env:
|
||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|