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 # The optional Gitee fallback has its own bounded retry budget and may be # enabled during a cross-border incident. Normal releases skip that step. timeout-minutes: 120 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 }} mirror-gitee-release: # Keep the optional cross-border fallback out of publish-release so all # pre-sync work has an independently provable budget. if: ${{ vars.ENABLE_GITEE_UPLOAD_FALLBACK == 'true' }} needs: - release - publish-release runs-on: ubuntu-latest timeout-minutes: 120 steps: - name: Check out repository uses: actions/checkout@v4 timeout-minutes: 5 - name: Restore finalized distribution files uses: actions/download-artifact@v4 timeout-minutes: 10 with: name: finalized-release-dist path: dist - name: Mirror release to Gitee (China) timeout-minutes: 100 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 }}