Files
larksuite__cli/.github/workflows/release.yml
zhaoyukun.yk 4dfe1b7dc0 feat(auth): support managed external credentials
Add Extended-only direct, credential-proxy, and platform-proxy modes while keeping Standard behavior and external-credential product details isolated behind source-neutral runtime capabilities.

Fail closed across helper execution, proxy routing, streams, managed file transfers, and unsupported event flows. Harden system configuration, edition updates, and release publication.

Cover Standard compatibility and Extended integration with contract tests, repository-local three-mode E2E, native trust checks, and CI and release gates.
2026-08-12 14:55:41 +08:00

634 lines
30 KiB
YAML

name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: read
env:
RELEASE_GO_VERSION: '1.26.5'
concurrency:
group: release-${{ github.ref_name }}
cancel-in-progress: false
jobs:
preflight:
runs-on: ubuntu-22.04
permissions:
contents: read
outputs:
source_sha: ${{ steps.validate.outputs.source_sha }}
version: ${{ steps.validate.outputs.version }}
channel: ${{ steps.validate.outputs.channel }}
prerelease: ${{ steps.validate.outputs.prerelease }}
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: '22.14.0'
package-manager-cache: false
- name: Refuse to rebuild a published release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
if gh release view "$TAG" --json isDraft -q .isDraft 2>/dev/null | grep -Fxq false; then
echo "Release ${TAG} is already public. Do not re-run the workflow; publish a new version instead." >&2
exit 1
fi
- name: Validate protected release tag
id: validate
env:
REF_PROTECTED: ${{ github.ref_protected }}
REPOSITORY: ${{ github.repository }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
[[ "$REPOSITORY" == "larksuite/cli" ]] || { echo "Release tags are accepted only from larksuite/cli." >&2; exit 1; }
[[ "$REF_PROTECTED" == "true" ]] || { echo "Release tag ${TAG} must be protected by a repository ruleset." >&2; exit 1; }
preflight_file="${RUNNER_TEMP}/release-preflight.json"
node scripts/release-preflight.js --tag "$TAG" > "$preflight_file"
git fetch --no-tags origin main
head_sha="$(git rev-parse --verify 'HEAD^{commit}')"
tag_sha="$(git rev-parse --verify "refs/tags/${TAG}^{commit}")"
[[ "$tag_sha" == "$head_sha" ]] || { echo "Tag ${TAG} does not resolve to checked-out HEAD." >&2; exit 1; }
main_sha="$(git rev-parse FETCH_HEAD)"
source_in_main=false
if git merge-base --is-ancestor "$head_sha" "$main_sha"; then
source_in_main=true
fi
node - "$preflight_file" "$head_sha" "$main_sha" "$source_in_main" "$GITHUB_OUTPUT" <<'NODE'
const fs = require("node:fs");
const { validateReleaseSourcePolicy } = require("./scripts/release-preflight");
const [file, sourceSha, mainSha, sourceInMain, output] = process.argv.slice(2);
const result = JSON.parse(fs.readFileSync(file, "utf8"));
if (result?.ok !== true || !["stable", "beta"].includes(result.data?.releaseChannel)) {
throw new Error("release preflight returned an invalid success payload");
}
const channel = result.data.releaseChannel;
const sourcePolicy = validateReleaseSourcePolicy(channel, sourceSha, mainSha, sourceInMain === "true");
if (!sourcePolicy.ok) {
throw new Error(sourcePolicy.error.message);
}
if (sourcePolicy.data.warning) {
console.log(`::warning title=Unexpected beta source::${sourcePolicy.data.warning}`);
}
fs.appendFileSync(output, `source_sha=${sourceSha}\nversion=${result.data.tagVersion}\nchannel=${channel}\nprerelease=${channel === "beta"}\n`);
NODE
build-sign-notarize:
needs: preflight
runs-on: ubuntu-22.04
timeout-minutes: 45
# This is the single approval boundary for the release. It grants access to
# Apple signing material before the candidate is built.
environment: npm-production
permissions:
contents: read
id-token: write
attestations: write
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version: ${{ env.RELEASE_GO_VERSION }}
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.x'
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: '22.14.0'
registry-url: 'https://registry.npmjs.org'
package-manager-cache: false
- name: Fetch build metadata
run: python3 scripts/fetch_meta.py
- name: Prepare Apple notarization key
env:
MACOS_NOTARY_ISSUER_ID: ${{ vars.MACOS_NOTARY_ISSUER_ID }}
MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }}
MACOS_NOTARY_KEY_ID: ${{ vars.MACOS_NOTARY_KEY_ID }}
MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }}
MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }}
MACOS_TEAM_ID: ${{ vars.MACOS_TEAM_ID }}
run: |
set -euo pipefail
set +x
for name in MACOS_SIGN_P12 MACOS_SIGN_PASSWORD MACOS_NOTARY_KEY MACOS_TEAM_ID MACOS_NOTARY_KEY_ID MACOS_NOTARY_ISSUER_ID; do
[[ -n "${!name:-}" ]] || { echo "Required Apple release input ${name} is not configured." >&2; exit 1; }
done
umask 077
notary_key="$(mktemp "${RUNNER_TEMP}/macos-notary-key.XXXXXX")"
if ! printf '%s' "$MACOS_NOTARY_KEY" | base64 --decode > "$notary_key"; then
rm -f -- "$notary_key"
echo "MACOS_NOTARY_KEY must be base64-encoded P8 content." >&2
exit 1
fi
chmod 0600 "$notary_key"
grep -Fxq -- '-----BEGIN PRIVATE KEY-----' "$notary_key" || {
rm -f -- "$notary_key"
echo "MACOS_NOTARY_KEY did not decode to a P8 private key." >&2
exit 1
}
printf 'MACOS_NOTARY_KEY_PATH=%s\n' "$notary_key" >> "$GITHUB_ENV"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with:
# v2.17.1 is the first tested version with this macOS notarization configuration.
version: v2.17.1
args: release --clean --skip=publish
env:
MACOS_NOTARY_ISSUER_ID: ${{ vars.MACOS_NOTARY_ISSUER_ID }}
MACOS_NOTARY_KEY_ID: ${{ vars.MACOS_NOTARY_KEY_ID }}
MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }}
MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }}
- name: Clean up Apple notarization key
if: ${{ always() }}
run: |
set -euo pipefail
set +x
[[ -z "${MACOS_NOTARY_KEY_PATH:-}" ]] || rm -f -- "$MACOS_NOTARY_KEY_PATH"
- name: Verify release Go toolchain
run: |
set -euo pipefail
mapfile -d '' release_binaries < <(
find dist -type f \( -name lark-cli -o -name lark-cli.exe \) -print0
)
(( ${#release_binaries[@]} > 0 )) || {
echo "GoReleaser produced no binaries to verify." >&2
exit 1
}
expected="go${RELEASE_GO_VERSION}"
for binary in "${release_binaries[@]}"; do
actual="$(go version -m "$binary" | awk 'NR == 1 { print $NF }')"
[[ "$actual" == "$expected" ]] || {
echo "Release binary ${binary} was built with ${actual:-an unknown Go version}; expected ${expected}." >&2
exit 1
}
echo "Verified ${binary}: ${actual}"
done
- name: Build release candidate
env:
VERSION: ${{ needs.preflight.outputs.version }}
run: |
set -euo pipefail
# checksums.txt covers the Extended installers, so they must sit
# alongside the archives before the digests are verified.
cp scripts/install-extended.sh scripts/install-extended.ps1 dist/
(cd dist && sha256sum --check checksums.txt)
mkdir release-candidate
cp dist/*.tar.gz dist/*.zip dist/checksums.txt release-candidate/
cp dist/install-extended.sh dist/install-extended.ps1 release-candidate/
cp dist/CHANGELOG.md release-candidate/CHANGELOG.md
cp dist/checksums.txt checksums.txt
npm install --global npm@11.16.0
pack_json="$(npm pack --ignore-scripts --json --pack-destination release-candidate)"
node - "$pack_json" "$VERSION" <<'NODE'
const [payload, version] = process.argv.slice(2);
const packs = JSON.parse(payload);
if (!Array.isArray(packs) || packs.length !== 1 || packs[0]?.name !== "@larksuite/cli" || packs[0]?.version !== version || !/^[^/\\]+\.tgz$/.test(packs[0]?.filename || "")) {
throw new Error("npm pack did not produce the expected release package");
}
NODE
- name: Verify release edition identities
env:
VERSION: ${{ needs.preflight.outputs.version }}
run: |
set -euo pipefail
work="$(mktemp -d "${RUNNER_TEMP}/release-editions.XXXXXX")"
trap 'rm -rf -- "$work"' EXIT
for edition in standard extended; do
prefix=lark-cli
[[ "$edition" != "extended" ]] || prefix=lark-cli-extended
archive="dist/${prefix}-${VERSION}-linux-amd64.tar.gz"
mkdir -p "$work/$edition"
tar -xzf "$archive" -C "$work/$edition" lark-cli
metadata="$("$work/$edition/lark-cli" version --json)"
[[ "$(jq -r .edition <<<"$metadata")" == "$edition" ]] || { echo "${archive} does not report edition ${edition}." >&2; exit 1; }
[[ "$(jq -r .version <<<"$metadata")" == "$VERSION" ]] || { echo "${archive} does not report version ${VERSION}." >&2; exit 1; }
done
- name: Verify release platform asset matrix
env:
VERSION: ${{ needs.preflight.outputs.version }}
run: bash scripts/verify-release-assets.sh dist "$VERSION"
- name: Attest release archives
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2
with:
subject-path: |
dist/*.tar.gz
dist/*.zip
dist/checksums.txt
dist/install-extended.sh
dist/install-extended.ps1
- name: Upload release candidate
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: release-candidate-${{ github.run_id }}
path: release-candidate/
if-no-files-found: error
overwrite: true
create-draft-release:
needs: [preflight, build-sign-notarize]
runs-on: ubuntu-22.04
timeout-minutes: 15
permissions:
contents: write
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
ref: ${{ needs.preflight.outputs.source_sha }}
fetch-depth: 0
persist-credentials: false
- name: Download release candidate
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: release-candidate-${{ github.run_id }}
path: release-candidate
- name: Verify tag still points to source commit
env:
SOURCE_SHA: ${{ needs.preflight.outputs.source_sha }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
git fetch --no-tags origin "refs/tags/${TAG}:refs/tags/${TAG}"
[[ "$(git rev-parse "refs/tags/${TAG}^{commit}")" == "$SOURCE_SHA" ]] || { echo "Release tag changed after preflight." >&2; exit 1; }
- name: Create or reuse Draft Release
env:
GH_TOKEN: ${{ github.token }}
PRERELEASE: ${{ needs.preflight.outputs.prerelease }}
SOURCE_SHA: ${{ needs.preflight.outputs.source_sha }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
release_notes=release-candidate/CHANGELOG.md
release_assets=(release-candidate/*.tar.gz release-candidate/*.zip release-candidate/checksums.txt release-candidate/install-extended.sh release-candidate/install-extended.ps1)
release_error="$(mktemp "${RUNNER_TEMP}/release-view.XXXXXX")"
trap 'rm -f -- "$release_error"' EXIT
verify_release_metadata() {
node -e '
const [metadata, sourceSha, prerelease] = process.argv.slice(1);
const release = JSON.parse(metadata);
if (release.isDraft !== true || release.targetCommitish !== sourceSha || release.isPrerelease !== (prerelease === "true")) {
throw new Error("Release metadata does not match this release candidate.");
}
' "$1" "$SOURCE_SHA" "$PRERELEASE"
}
if metadata="$(gh release view "$TAG" --json isDraft,isPrerelease,targetCommitish 2>"$release_error")"; then
verify_release_metadata "$metadata"
existing="$(mktemp -d "${RUNNER_TEMP}/draft-release.XXXXXX")"
trap 'rm -f -- "$release_error"; rm -rf -- "$existing"' EXIT
expected_names=("${release_assets[@]##*/}")
mapfile -t existing_names < <(gh release view "$TAG" --json assets -q '.assets[].name' | sort)
for name in "${existing_names[@]}"; do
printf '%s\n' "${expected_names[@]}" | grep -Fxq "$name" || {
echo "Draft Release contains unexpected asset ${name}." >&2
exit 1
}
done
if (( ${#existing_names[@]} > 0 )); then
gh release download "$TAG" --dir "$existing"
for name in "${existing_names[@]}"; do
if [[ "$name" == "checksums.txt" ]]; then
cmp --silent release-candidate/checksums.txt "$existing/$name" || { echo "Draft Release checksums do not match the current candidate." >&2; exit 1; }
continue
fi
checksum_line="$(awk -v name="$name" '$2 == name { print }' release-candidate/checksums.txt)"
[[ "$(printf '%s\n' "$checksum_line" | sed '/^$/d' | wc -l | tr -d '[:space:]')" == "1" ]] || { echo "Candidate has no unique checksum for ${name}." >&2; exit 1; }
printf '%s\n' "$checksum_line" | (cd "$existing" && sha256sum --check -)
done
fi
missing_assets=()
for asset in "${release_assets[@]}"; do
name="${asset##*/}"
printf '%s\n' "${existing_names[@]}" | grep -Fxq "$name" || missing_assets+=("$asset")
done
if (( ${#missing_assets[@]} > 0 )); then
gh release upload "$TAG" "${missing_assets[@]}"
fi
diff --brief \
<(printf '%s\n' "${expected_names[@]}" | sort) \
<(gh release view "$TAG" --json assets -q '.assets[].name' | sort)
gh release edit "$TAG" --notes-file "$release_notes"
exit 0
else
if ! grep -Eqi 'HTTP 404|release not found' "$release_error"; then
cat "$release_error" >&2
echo "Could not determine whether the Release already exists." >&2
exit 1
fi
args=("$TAG" --target "$SOURCE_SHA" --title "$TAG" --draft)
[[ "$PRERELEASE" != "true" ]] || args+=(--prerelease)
gh release create "${args[@]}" --verify-tag --notes-file "$release_notes"
verify_release_metadata "$(gh release view "$TAG" --json isDraft,isPrerelease,targetCommitish)"
fi
gh release edit "$TAG" --notes-file "$release_notes"
gh release upload "$TAG" "${release_assets[@]}"
diff --brief \
<(printf '%s\n' "${release_assets[@]##*/}" | sort) \
<(gh release view "$TAG" --json assets -q '.assets[].name' | sort)
verify-macos:
needs: [preflight, build-sign-notarize, create-draft-release]
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- runner: macos-15-intel
arch: amd64
- runner: macos-15
arch: arm64
runs-on: ${{ matrix.runner }}
timeout-minutes: 20
steps:
- name: Download release candidate
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: release-candidate-${{ github.run_id }}
path: release-candidate
- name: Verify notarized macOS binaries
env:
ARCH: ${{ matrix.arch }}
MACOS_TEAM_ID: ${{ vars.MACOS_TEAM_ID }}
VERSION: ${{ needs.preflight.outputs.version }}
run: |
set -euo pipefail
[[ -n "$MACOS_TEAM_ID" ]] || { echo "MACOS_TEAM_ID is not configured." >&2; exit 1; }
work="$(mktemp -d "${RUNNER_TEMP}/macos-release.XXXXXX")"
trap 'rm -rf -- "$work" checksum.txt' EXIT
escaped_version="${VERSION//./\\.}"
# Every published macOS archive is verified, so each distributed
# edition is checked on both architectures.
for archive in "lark-cli-${VERSION}-darwin-${ARCH}.tar.gz" "lark-cli-extended-${VERSION}-darwin-${ARCH}.tar.gz"; do
awk -v archive="$archive" '$2 == archive { print }' release-candidate/checksums.txt > checksum.txt
[[ "$(wc -l < checksum.txt | tr -d '[:space:]')" == "1" ]] || { echo "checksums.txt must contain exactly one entry for ${archive}." >&2; exit 1; }
(cd release-candidate && shasum -a 256 -c ../checksum.txt)
extracted="$work/${archive%.tar.gz}"
mkdir -p "$extracted"
tar -xzf "release-candidate/$archive" -C "$extracted"
binary="$extracted/lark-cli"
[[ -f "$binary" && ! -L "$binary" ]] || { echo "Archive ${archive} did not contain a regular lark-cli binary." >&2; exit 1; }
codesign --verify --strict --verbose=4 "$binary"
details="$(codesign -dv --verbose=4 "$binary" 2>&1)"
grep -Eq '^Authority=Developer ID Application: .+' <<<"$details"
grep -Fxq "TeamIdentifier=${MACOS_TEAM_ID}" <<<"$details"
grep -Eq '^CodeDirectory .*flags=0x[0-9A-Fa-f]+[(][^)]*runtime[^)]*[)]' <<<"$details"
grep -Eq '^Timestamp=.+' <<<"$details"
codesign --verify --strict --verbose=4 --check-notarization -R='notarized' "$binary"
"$binary" --version | grep -Eq "(^|[^0-9A-Za-z.])${escaped_version}([^0-9A-Za-z.]|$)"
done
publish-github:
needs: [preflight, create-draft-release, verify-macos]
runs-on: ubuntu-22.04
timeout-minutes: 15
permissions:
contents: write
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
ref: ${{ needs.preflight.outputs.source_sha }}
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: '22.14.0'
registry-url: 'https://registry.npmjs.org'
package-manager-cache: false
- name: Download release candidate
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: release-candidate-${{ github.run_id }}
path: release-candidate
- name: Verify tag still points to source commit
env:
SOURCE_SHA: ${{ needs.preflight.outputs.source_sha }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
git fetch --no-tags origin "refs/tags/${TAG}:refs/tags/${TAG}"
[[ "$(git rev-parse "refs/tags/${TAG}^{commit}")" == "$SOURCE_SHA" ]] || { echo "Release tag changed after preflight." >&2; exit 1; }
- name: Verify Draft assets match the candidate
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
existing="$(mktemp -d "${RUNNER_TEMP}/draft-release.XXXXXX")"
trap 'rm -rf -- "$existing"' EXIT
gh release download "$TAG" --dir "$existing"
cmp --silent release-candidate/checksums.txt "$existing/checksums.txt" || { echo "Draft Release checksums do not match the verified candidate." >&2; exit 1; }
(cd "$existing" && sha256sum --check checksums.txt)
diff --brief \
<(find release-candidate -maxdepth 1 -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name checksums.txt -o -name install-extended.sh -o -name install-extended.ps1 \) -printf '%f\n' | sort) \
<(gh release view "$TAG" --json assets -q '.assets[].name' | sort)
- name: Refuse GitHub publication if npm channel is newer
env:
CHANNEL: ${{ needs.preflight.outputs.channel }}
VERSION: ${{ needs.preflight.outputs.version }}
run: |
set -euo pipefail
dist_tag=latest
[[ "$CHANNEL" != "beta" ]] || dist_tag=beta
if ! current="$(npm view @larksuite/cli "dist-tags.${dist_tag}" --json | tr -d '"[:space:]')"; then
echo "Could not determine the npm ${dist_tag} dist-tag before GitHub publication." >&2
exit 1
fi
[[ -n "$current" && "$current" != "null" ]] || exit 0
node - "$VERSION" "$current" "$dist_tag" <<'NODE'
const { compareReleaseVersions } = require("./scripts/release-publish-policy");
const [version, current, distTag] = process.argv.slice(2);
if (compareReleaseVersions(current, version) >= 0) {
throw new Error(`npm ${distTag} already points to ${current}; retain no GitHub Release for ${version}.`);
}
NODE
- name: Publish verified Draft Release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.ref_name }}
run: gh release edit "$TAG" --draft=false
publish-npm:
needs: [preflight, build-sign-notarize, publish-github]
runs-on: ubuntu-22.04
timeout-minutes: 15
concurrency:
group: npm-release-${{ needs.preflight.outputs.channel }}
queue: max
cancel-in-progress: false
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
ref: ${{ needs.preflight.outputs.source_sha }}
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: '22.14.0'
registry-url: 'https://registry.npmjs.org'
package-manager-cache: false
- name: Download release candidate
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: release-candidate-${{ github.run_id }}
path: release-candidate
- name: Install pinned npm
run: npm install --global npm@11.16.0
- name: Verify tag still points to source commit
env:
SOURCE_SHA: ${{ needs.preflight.outputs.source_sha }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
git fetch --no-tags origin "refs/tags/${TAG}:refs/tags/${TAG}"
[[ "$(git rev-parse "refs/tags/${TAG}^{commit}")" == "$SOURCE_SHA" ]] || { echo "Release tag changed after preflight." >&2; exit 1; }
- name: Publish or verify npm package
env:
CHANNEL: ${{ needs.preflight.outputs.channel }}
VERSION: ${{ needs.preflight.outputs.version }}
run: |
set -euo pipefail
shopt -s nullglob
packages=(release-candidate/*.tgz)
(( ${#packages[@]} == 1 )) || { echo "Expected exactly one npm package." >&2; exit 1; }
tgz="${packages[0]}"
tar -xOzf "$tgz" package/checksums.txt > "${RUNNER_TEMP}/checksums.txt"
cmp --silent release-candidate/checksums.txt "${RUNNER_TEMP}/checksums.txt" || { echo "npm package checksums do not match the release candidate." >&2; exit 1; }
integrity="$(node - "$tgz" <<'NODE'
const crypto = require("node:crypto");
const fs = require("node:fs");
const hash = crypto.createHash("sha512");
hash.update(fs.readFileSync(process.argv[2]));
process.stdout.write(`sha512-${hash.digest("base64")}`);
NODE
)"
dist_tag=latest
[[ "$CHANNEL" != "beta" ]] || dist_tag=beta
version_exists=false
integrity_matches=false
version_error="$(mktemp "${RUNNER_TEMP}/npm-version.XXXXXX")"
trap 'rm -f -- "$version_error"' EXIT
if npm view "@larksuite/cli@${VERSION}" version --json >/dev/null 2>"$version_error"; then
version_exists=true
published="$(npm view "@larksuite/cli@${VERSION}" dist.integrity --json | tr -d '"[:space:]')"
[[ "$published" != "$integrity" ]] || integrity_matches=true
elif ! node - "$version_error" <<'NODE'
const fs = require("node:fs");
const { isNpmVersionMissing } = require("./scripts/release-publish-policy");
process.exit(isNpmVersionMissing(fs.readFileSync(process.argv[2], "utf8")) ? 0 : 1);
NODE
then
cat "$version_error" >&2
echo "Could not determine whether npm version ${VERSION} already exists." >&2
exit 1
fi
if ! current="$(npm view @larksuite/cli "dist-tags.${dist_tag}" --json | tr -d '"[:space:]')"; then
echo "Could not determine the npm ${dist_tag} dist-tag." >&2
exit 1
fi
[[ -n "$current" && "$current" != "null" ]] || current=null
decision="$(node - "$VERSION" "$version_exists" "$integrity_matches" "$current" <<'NODE'
const { decideNpmPublish } = require("./scripts/release-publish-policy");
const [version, versionExists, integrityMatches, channelVersion] = process.argv.slice(2);
process.stdout.write(JSON.stringify(decideNpmPublish({
version,
versionExists: versionExists === "true",
integrityMatches: integrityMatches === "true",
channelVersion: channelVersion === "null" ? null : channelVersion,
})));
NODE
)"
action="$(node -e 'process.stdout.write(JSON.parse(process.argv[1]).action)' "$decision")"
case "$action" in
publish)
npm publish "./$tgz" --access public --provenance --tag "$dist_tag"
;;
advance-tag)
npm dist-tag add "@larksuite/cli@${VERSION}" "$dist_tag"
;;
verify)
echo "npm ${dist_tag} already points to ${VERSION} or a newer verified version."
;;
reject)
node -e 'console.error(JSON.parse(process.argv[1]).reason); process.exit(1)' "$decision"
;;
*)
echo "Unexpected npm publish decision: ${action}" >&2
exit 1
;;
esac
retry-guidance:
needs: [preflight, build-sign-notarize, create-draft-release, verify-macos, publish-github, publish-npm]
if: ${{ always() && (needs.preflight.result == 'failure' || needs.build-sign-notarize.result == 'failure' || needs.create-draft-release.result == 'failure' || needs.verify-macos.result == 'failure' || needs.publish-github.result == 'failure' || needs.publish-npm.result == 'failure') }}
runs-on: ubuntu-22.04
permissions:
contents: read
steps:
- name: Write retry guidance
env:
PREFLIGHT_RESULT: ${{ needs.preflight.result }}
BUILD_RESULT: ${{ needs.build-sign-notarize.result }}
DRAFT_RESULT: ${{ needs.create-draft-release.result }}
VERIFY_RESULT: ${{ needs.verify-macos.result }}
GITHUB_RESULT: ${{ needs.publish-github.result }}
NPM_RESULT: ${{ needs.publish-npm.result }}
run: |
set -euo pipefail
{
echo "## Release retry guidance"
echo
echo "This job only records recovery guidance; it does not retry or publish anything."
echo
echo "| Job | Result |"
echo "| --- | --- |"
echo "| preflight | ${PREFLIGHT_RESULT} |"
echo "| build-sign-notarize | ${BUILD_RESULT} |"
echo "| create-draft-release | ${DRAFT_RESULT} |"
echo "| verify-macos | ${VERIFY_RESULT} |"
echo "| publish-github | ${GITHUB_RESULT} |"
echo "| publish-npm | ${NPM_RESULT} |"
printf '%s\n' \
'' \
'Select the recovery action from the failed-step diagnosis. If a Draft or public Release already exists, use **Re-run failed jobs**, not **Re-run all jobs**:' \
'' \
'- **preflight:** network or fetch failure → retry preflight. Version/tag validation failure → correct it, then create a new tag.' \
'- **build-sign-notarize:** transient build/service failure → retry build. Code or release configuration issue → correct it, then create a new tag.' \
'- **create-draft-release:** GitHub Draft Release API/upload failure → retry draft; a partial Draft is verified and only missing assets are uploaded. Release-candidate inconsistency → delete the Draft, then retry build.' \
'- **verify-macos:** runner or network failure → retry only the failed matrix child. Checksum, signing, notarization, or runtime failure → delete the Draft, then retry build.' \
'- **publish-github:** GitHub publish network failure → retry GitHub publish. If the npm channel is already newer, delete the retained Draft Release and publish a higher version. Install issue → delete the Draft, then retry build. Tag/assets inconsistency → stop and publish a new version.' \
'- **publish-npm:** network failure or uncertain publish outcome → retry npm only after verifying whether that version already exists. Integrity mismatch → publish a new version. If the npm channel is already newer, delete the public GitHub Release and publish a higher version.'
} >> "$GITHUB_STEP_SUMMARY"