mirror of
https://github.com/larksuite/cli.git
synced 2026-08-03 08:32:46 +08:00
Compare commits
8 Commits
v1.0.79-be
...
ci/macos-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdfac79ba7 | ||
|
|
16e3612829 | ||
|
|
87583e796c | ||
|
|
6cda02460e | ||
|
|
0c7b36553e | ||
|
|
752e65f075 | ||
|
|
7eedaedcf9 | ||
|
|
e20e6d3c2a |
52
.github/workflows/release.yml
vendored
52
.github/workflows/release.yml
vendored
@@ -30,6 +30,7 @@ jobs:
|
||||
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
|
||||
with:
|
||||
node-version: '22.14.0'
|
||||
package-manager-cache: false
|
||||
|
||||
- name: Refuse to rebuild a published release
|
||||
env:
|
||||
@@ -110,6 +111,9 @@ jobs:
|
||||
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 }}
|
||||
@@ -217,7 +221,6 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PRERELEASE: ${{ needs.preflight.outputs.prerelease }}
|
||||
REHEARSAL_PARTIAL_DRAFT: true
|
||||
SOURCE_SHA: ${{ needs.preflight.outputs.source_sha }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
@@ -284,11 +287,6 @@ jobs:
|
||||
verify_release_metadata "$(gh release view "$TAG" --json isDraft,isPrerelease,targetCommitish)"
|
||||
fi
|
||||
gh release edit "$TAG" --notes-file "$release_notes"
|
||||
if [[ "${REHEARSAL_PARTIAL_DRAFT:-}" == "true" && "$GITHUB_RUN_ATTEMPT" == "1" ]]; then
|
||||
gh release upload "$TAG" "${release_assets[0]}"
|
||||
echo "S2 rehearsal: partial Draft uploaded; re-run failed jobs to resume." >&2
|
||||
exit 1
|
||||
fi
|
||||
gh release upload "$TAG" "${release_assets[@]}"
|
||||
diff --brief \
|
||||
<(printf '%s\n' "${release_assets[@]##*/}" | sort) \
|
||||
@@ -318,7 +316,6 @@ jobs:
|
||||
env:
|
||||
ARCH: ${{ matrix.arch }}
|
||||
MACOS_TEAM_ID: ${{ vars.MACOS_TEAM_ID }}
|
||||
REHEARSAL_EXPECTED_TEAM_ID: TEST_INVALID_TEAM
|
||||
VERSION: ${{ needs.preflight.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -334,13 +331,13 @@ jobs:
|
||||
[[ -f "$binary" && ! -L "$binary" ]] || { echo "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)"
|
||||
[[ -z "${REHEARSAL_EXPECTED_TEAM_ID:-}" ]] || MACOS_TEAM_ID="$REHEARSAL_EXPECTED_TEAM_ID"
|
||||
grep -Eq '^Authority=Developer ID Application: .+' <<<"$details"
|
||||
grep -Fxq "TeamIdentifier=${MACOS_TEAM_ID}" <<<"$details"
|
||||
grep -Eq '^flags=0x[0-9A-Fa-f]+\(.*runtime.*\)' <<<"$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 -Fq "$VERSION"
|
||||
escaped_version="${VERSION//./\\.}"
|
||||
"$binary" --version | grep -Eq "(^|[^0-9A-Za-z.])${escaped_version}([^0-9A-Za-z.]|$)"
|
||||
|
||||
publish-github:
|
||||
needs: [preflight, create-draft-release, verify-macos]
|
||||
@@ -354,6 +351,11 @@ jobs:
|
||||
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:
|
||||
@@ -381,6 +383,26 @@ jobs:
|
||||
diff --brief \
|
||||
<(find release-candidate -maxdepth 1 -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name checksums.txt \) -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 }}
|
||||
@@ -532,12 +554,12 @@ jobs:
|
||||
echo "| publish-npm | ${NPM_RESULT} |"
|
||||
printf '%s\n' \
|
||||
'' \
|
||||
'Select the recovery action from the failed-step diagnosis:' \
|
||||
'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 → retry build.' \
|
||||
'- **verify-macos:** runner or network failure → retry only the failed matrix child. Checksum, signing, notarization, or runtime failure → retry build.' \
|
||||
'- **publish-github:** GitHub publish network failure → retry GitHub publish. Install issue → 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.'
|
||||
'- **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"
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
version: 2
|
||||
|
||||
before:
|
||||
hooks:
|
||||
- python3 scripts/fetch_meta.py
|
||||
|
||||
builds:
|
||||
- id: lark-cli
|
||||
binary: lark-cli
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@larksuite/cli",
|
||||
"version": "1.0.79-beta.3",
|
||||
"version": "1.0.80",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@larksuite/cli",
|
||||
"version": "1.0.79-beta.3",
|
||||
"version": "1.0.80",
|
||||
"cpu": [
|
||||
"x64",
|
||||
"arm64",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@larksuite/cli",
|
||||
"version": "1.0.79-beta.3",
|
||||
"version": "1.0.80",
|
||||
"description": "The official CLI for Lark/Feishu open platform",
|
||||
"bin": {
|
||||
"lark-cli": "scripts/run.js"
|
||||
|
||||
@@ -33,8 +33,8 @@ function validateReleaseSourcePolicy(releaseChannel, sourceSha, mainSha, sourceI
|
||||
}
|
||||
|
||||
if (releaseChannel === "beta") {
|
||||
const warning = sourceSha === mainSha
|
||||
? "Beta release tag points to the current origin/main HEAD; check whether an unintended beta version was merged into main."
|
||||
const warning = sourceInMain
|
||||
? "Beta release tag points to a commit already contained in origin/main; check whether an unintended beta version was merged into main."
|
||||
: null;
|
||||
return { ok: true, data: { warning } };
|
||||
}
|
||||
|
||||
@@ -163,8 +163,8 @@ describe("validateReleaseSourcePolicy", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("warns but accepts a beta release tag pointing to the current main head", () => {
|
||||
const result = validateReleaseSourcePolicy("beta", mainSha, mainSha, true);
|
||||
it("warns but accepts a beta release tag pointing to a commit already in main", () => {
|
||||
const result = validateReleaseSourcePolicy("beta", betaSha, mainSha, true);
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.match(result.data.warning, /unintended beta version was merged into main/);
|
||||
|
||||
@@ -10,13 +10,13 @@ ruby -ropen3 -ryaml <<'RUBY'
|
||||
workflow = YAML.load_file(".github/workflows/release.yml")
|
||||
goreleaser = YAML.load_file(".goreleaser.yml")
|
||||
|
||||
def fail(message)
|
||||
def contract_error(message)
|
||||
abort("release workflow contract: #{message}")
|
||||
end
|
||||
|
||||
def expect_equal(actual, expected, description)
|
||||
return if actual == expected
|
||||
fail("#{description}; expected #{expected.inspect}, got #{actual.inspect}")
|
||||
contract_error("#{description}; expected #{expected.inspect}, got #{actual.inspect}")
|
||||
end
|
||||
|
||||
def scalar_values(value)
|
||||
@@ -45,7 +45,7 @@ jobs.each do |job_name, job|
|
||||
next unless run.is_a?(String)
|
||||
|
||||
_stdout, stderr, status = Open3.capture3("bash", "-n", stdin_data: run)
|
||||
fail("#{job_name}/#{step["name"]} has invalid bash syntax: #{stderr}") unless status.success?
|
||||
contract_error("#{job_name}/#{step["name"]} has invalid bash syntax: #{stderr}") unless status.success?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -95,7 +95,7 @@ expected_timeouts.each do |job_name, timeout|
|
||||
end
|
||||
|
||||
expect_equal(jobs.fetch("build-sign-notarize").fetch("environment"), "npm-production", "signing approval environment")
|
||||
fail("publish-npm must not request a second Environment approval") if jobs.fetch("publish-npm").key?("environment")
|
||||
contract_error("publish-npm must not request a second Environment approval") if jobs.fetch("publish-npm").key?("environment")
|
||||
expect_equal(jobs.fetch("publish-npm").fetch("concurrency"), {
|
||||
"group" => "npm-release-${{ needs.preflight.outputs.channel }}",
|
||||
"queue" => "max",
|
||||
@@ -111,7 +111,10 @@ retry_steps = retry_guidance.fetch("steps")
|
||||
expect_equal(retry_steps.length, 1, "number of retry guidance steps")
|
||||
retry_step = retry_steps.first
|
||||
expect_equal(retry_step.fetch("name"), "Write retry guidance", "retry guidance step name")
|
||||
fail("retry guidance must write to the GitHub step summary") unless retry_step.fetch("run").include?("GITHUB_STEP_SUMMARY")
|
||||
contract_error("retry guidance must write to the GitHub step summary") unless retry_step.fetch("run").include?("GITHUB_STEP_SUMMARY")
|
||||
contract_error("retry guidance must direct recoveries to failed-job retries") unless retry_step.fetch("run").include?("Re-run failed jobs")
|
||||
contract_error("retry guidance must explain Draft cleanup before a rebuild") unless retry_step.fetch("run").include?("delete the Draft, then retry build")
|
||||
contract_error("retry guidance must explain public Release cleanup after npm policy rejection") unless retry_step.fetch("run").include?("delete the public GitHub Release")
|
||||
|
||||
signing_references = %w[
|
||||
secrets.MACOS_SIGN_P12
|
||||
@@ -137,6 +140,12 @@ jobs.each do |job_name, job|
|
||||
)
|
||||
end
|
||||
|
||||
build_steps = jobs.fetch("build-sign-notarize").fetch("steps")
|
||||
fetch_metadata_index = build_steps.index { |step| step["name"] == "Fetch build metadata" }
|
||||
prepare_key_index = build_steps.index { |step| step["name"] == "Prepare Apple notarization key" }
|
||||
contract_error("build metadata must be fetched before Apple credentials are prepared") unless fetch_metadata_index && prepare_key_index && fetch_metadata_index < prepare_key_index
|
||||
contract_error("build metadata must be fetched outside GoReleaser hooks") if goreleaser.dig("before", "hooks")&.include?("python3 scripts/fetch_meta.py")
|
||||
|
||||
macos = jobs.fetch("verify-macos")
|
||||
expect_equal(macos.fetch("strategy").fetch("matrix").fetch("include"), [
|
||||
{ "runner" => "macos-15-intel", "arch" => "amd64" },
|
||||
@@ -146,34 +155,45 @@ expect_equal(macos.fetch("runs-on"), "${{ matrix.runner }}", "macOS matrix runne
|
||||
macos_verify_step = macos.fetch("steps").find { |step| step["name"] == "Verify notarized macOS binary" }
|
||||
macos_verify_run = macos_verify_step&.fetch("run", nil)
|
||||
macos_download_step = macos.fetch("steps").find { |step| step["name"] == "Download release candidate" }
|
||||
fail("verify-macos must download the build candidate artifact") unless macos_download_step&.fetch("uses", nil)&.start_with?("actions/download-artifact@")
|
||||
fail("verify-macos must not download mutable Draft Release assets") if macos_verify_run&.include?("gh release download")
|
||||
fail("verify-macos must verify notarization through codesign") unless macos_verify_run&.include?("--check-notarization -R='notarized'")
|
||||
contract_error("verify-macos must download the build candidate artifact") unless macos_download_step&.fetch("uses", nil)&.start_with?("actions/download-artifact@")
|
||||
contract_error("verify-macos must not download mutable Draft Release assets") if macos_verify_run&.include?("gh release download")
|
||||
contract_error("verify-macos must verify notarization through codesign") unless macos_verify_run&.include?("--check-notarization -R='notarized'")
|
||||
contract_error("verify-macos must check Developer ID authority") unless macos_verify_run&.include?("^Authority=Developer ID Application: .+")
|
||||
contract_error("verify-macos must check the expected Team ID") unless macos_verify_run&.include?("TeamIdentifier=${MACOS_TEAM_ID}")
|
||||
contract_error("verify-macos must detect hardened runtime in CodeDirectory metadata") unless macos_verify_run&.include?("^CodeDirectory .*flags=0x")
|
||||
contract_error("verify-macos must check the signing timestamp") unless macos_verify_run&.include?("^Timestamp=.+")
|
||||
contract_error("verify-macos must match the complete release version") unless macos_verify_run&.include?("escaped_version")
|
||||
|
||||
draft_step = jobs.fetch("create-draft-release").fetch("steps").find { |step| step["name"] == "Create or reuse Draft Release" }
|
||||
draft_run = draft_step&.fetch("run", nil)
|
||||
fail("Draft Release creation must write generated release notes") unless draft_run&.include?("--notes-file")
|
||||
fail("Draft Release reuse must validate target commit and prerelease state") unless draft_run&.include?("targetCommitish") && draft_run.include?("isPrerelease")
|
||||
fail("Draft Release creation must target the validated source commit") unless draft_run&.include?("--target \"$SOURCE_SHA\"")
|
||||
fail("Draft Release creation must require the existing remote tag") unless draft_run&.include?("--verify-tag")
|
||||
contract_error("Draft Release creation must write generated release notes") unless draft_run&.include?("--notes-file")
|
||||
contract_error("Draft Release reuse must validate target commit and prerelease state") unless draft_run&.include?("targetCommitish") && draft_run.include?("isPrerelease")
|
||||
contract_error("Draft Release creation must target the validated source commit") unless draft_run&.include?("--target \"$SOURCE_SHA\"")
|
||||
contract_error("Draft Release creation must require the existing remote tag") unless draft_run&.include?("--verify-tag")
|
||||
|
||||
github_steps = jobs.fetch("publish-github").fetch("steps")
|
||||
github_check = github_steps.find { |step| step["name"] == "Verify Draft assets match the candidate" }
|
||||
fail("GitHub publication must verify Draft assets against the candidate") unless github_check&.fetch("run", nil)&.include?("release-candidate/checksums.txt")
|
||||
contract_error("GitHub publication must verify Draft assets against the candidate") unless github_check&.fetch("run", nil)&.include?("release-candidate/checksums.txt")
|
||||
github_npm_guard = github_steps.find { |step| step["name"] == "Refuse GitHub publication if npm channel is newer" }
|
||||
contract_error("GitHub publication must refuse a version behind the npm channel") unless github_npm_guard&.fetch("run", nil)&.include?("compareReleaseVersions")
|
||||
contract_error("GitHub publication must query the matching npm dist-tag") unless github_npm_guard&.fetch("run", nil)&.include?("dist-tags.${dist_tag}")
|
||||
|
||||
npm_steps = jobs.fetch("publish-npm").fetch("steps")
|
||||
pinned_npm = npm_steps.find { |step| step["name"] == "Install pinned npm" }
|
||||
fail("publish-npm must install npm 11.16.0 for trusted publishing") unless pinned_npm&.fetch("run", nil) == "npm install --global npm@11.16.0"
|
||||
contract_error("publish-npm must install npm 11.16.0 for trusted publishing") unless pinned_npm&.fetch("run", nil) == "npm install --global npm@11.16.0"
|
||||
publish_step = npm_steps.find { |step| step["name"] == "Publish or verify npm package" }
|
||||
fail("publish-npm must explicitly pass the candidate tarball as a local path") unless publish_step&.fetch("run", nil).include?('npm publish "./$tgz"')
|
||||
contract_error("publish-npm must explicitly pass the candidate tarball as a local path") unless publish_step&.fetch("run", nil).include?('npm publish "./$tgz"')
|
||||
contract_error("publish-npm must publish provenance under the selected channel tag") unless publish_step&.fetch("run", nil).include?('--provenance --tag "$dist_tag"')
|
||||
contract_error("beta releases must publish under the beta dist-tag") unless publish_step&.fetch("run", nil).include?('[[ "$CHANNEL" != "beta" ]] || dist_tag=beta')
|
||||
|
||||
action_references(workflow).each do |reference|
|
||||
fail("action is not pinned to a full commit SHA: #{reference}") unless reference.match?(%r{\A[^@]+@[0-9a-f]{40}\z})
|
||||
contract_error("action is not pinned to a full commit SHA: #{reference}") unless reference.match?(%r{\A[^@]+@[0-9a-f]{40}\z})
|
||||
end
|
||||
|
||||
notarize = goreleaser.fetch("notarize").fetch("macos")
|
||||
expect_equal(notarize.length, 1, "number of macOS notarization configurations")
|
||||
macos_notarize = notarize.first
|
||||
expect_equal(macos_notarize.fetch("enabled"), '{{ isEnvSet "MACOS_SIGN_P12" }}', "macOS notarization enablement")
|
||||
expect_equal(macos_notarize.fetch("ids"), ["lark-cli"], "notarized build IDs")
|
||||
expect_equal(macos_notarize.fetch("sign"), {
|
||||
"certificate" => "{{ .Env.MACOS_SIGN_P12 }}",
|
||||
@@ -186,6 +206,7 @@ expect_equal(macos_notarize.fetch("notarize"), {
|
||||
"wait" => true,
|
||||
"timeout" => "20m",
|
||||
}, "macOS notarization inputs")
|
||||
contract_error("GoReleaser must build a darwin release artifact") unless goreleaser.fetch("builds").any? { |build| build.fetch("goos").include?("darwin") }
|
||||
|
||||
puts "release workflow contract passed"
|
||||
RUBY
|
||||
|
||||
Reference in New Issue
Block a user