This is an automated email from the ASF dual-hosted git repository. EmryZhang pushed a commit to branch release/casbin-js-workflow-20261107 in repository https://gitbox.apache.org/repos/asf/casbin-casbin.js.git
commit e0c2d8ab39d12318f9511ae025f08977e2fb5e46 Author: Zhangyanrui123 <[email protected]> AuthorDate: Sat Nov 7 23:32:01 2026 +0800 ci: publish only from verified ASF source originals --- .github/scripts/release.py | 187 ++++++++++++++++++++++++++++++++++++ .github/scripts/test_release.py | 205 ++++++++++++++++++++++++++++++++++++++++ .github/workflows/ci.yml | 7 +- .github/workflows/release.yml | 157 ++++++++++++++++++------------ 4 files changed, 494 insertions(+), 62 deletions(-) diff --git a/.github/scripts/release.py b/.github/scripts/release.py new file mode 100644 index 0000000..dd30d98 --- /dev/null +++ b/.github/scripts/release.py @@ -0,0 +1,187 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import argparse +import gzip +import hashlib +import io +import json +import re +import subprocess +import tarfile +import tempfile +import tomllib +import urllib.parse +import urllib.request +from pathlib import Path + +PROJECT = "casbin-js" +COMPONENT = "casbin-js" +EXPECTED_PACKAGE = "apache-casbin.js" +MANIFEST = "package.json" +ASF_BASE = "https://downloads.apache.org/incubator/casbin" + + +def git(*args): + return subprocess.check_output(["git", *args]) + + +def metadata(tag): + match = re.fullmatch(r"v(\d+\.\d+\.\d+)(-rc[1-9]\d*)?", tag) + if not match: + raise ValueError("Use vX.Y.Z or vX.Y.Z-rcN (N >= 1)") + version, rc = match.groups() + manifest = git("show", f"{tag}:{MANIFEST}") + package = json.loads(manifest) if MANIFEST.endswith("json") else tomllib.loads(manifest.decode())["project"] + if package.get("name") != EXPECTED_PACKAGE: + raise ValueError("Committed package identity does not match the adopted name") + actual = package["version"] + if actual != version: + raise ValueError(f"Commit version {actual} does not match {version}; no version stamping") + name = f"apache-{PROJECT}-{version}-incubating-src" + return version, bool(rc), name + + +def inventory(data, name): + result = {} + with tarfile.open(fileobj=io.BytesIO(data), mode="r:*") as archive: + for member in archive: + parts = Path(member.name).parts + if not parts or parts[0] != name or ".." in parts or "\\" in member.name: + raise ValueError(f"Unexpected archive path: {member.name}") + if member.isdir(): + continue + if not member.isfile() or member.name in result: + raise ValueError(f"Link, special file or duplicate: {member.name}") + result[member.name] = (archive.extractfile(member).read(), member.mode & 0o777) + for required in ("LICENSE", "NOTICE", "DISCLAIMER", MANIFEST): + if f"{name}/{required}" not in result: + raise ValueError(f"Missing {required}") + return result + + +def source(tag, name): + data = git("archive", "--format=tar", f"--prefix={name}/", tag) + inventory(data, name) + return data + + +def download(url): + with urllib.request.urlopen(url, timeout=90) as response: + destination = urllib.parse.urlparse(response.url) + if ( + destination.scheme != "https" + or destination.hostname not in ("downloads.apache.org", "dlcdn.apache.org") + or not destination.path.startswith("/incubator/casbin/") + ): + raise ValueError("Download redirected outside the project's approved ASF release area") + return response.read() + + +def verify_signature(key_data, target, signature): + with tempfile.TemporaryDirectory() as scratch: + keyhome = Path(scratch) / "gnupg" + keyhome.mkdir(mode=0o700) + keys = Path(scratch) / "KEYS" + keys.write_bytes(key_data) + subprocess.run(["gpg", "--batch", "--homedir", str(keyhome), "--import", str(keys)], check=True) + subprocess.run(["gpg", "--batch", "--homedir", str(keyhome), "--verify", str(signature), str(target)], check=True) + + +def check_npm(path): + root = Path.cwd() + expected = json.loads((root / MANIFEST).read_text()) + files = {} + with tarfile.open(path) as archive: + for entry in archive: + parts = Path(entry.name).parts + if not parts or parts[0] != "package" or ".." in parts or "\\" in entry.name: + raise ValueError("Unsafe npm package entry") + if entry.isdir(): + continue + canonical = "/".join(parts[1:]) + if not entry.isfile() or canonical in files: + raise ValueError("npm package contains links, special files or duplicates") + files[canonical] = archive.extractfile(entry).read() + actual = json.loads(files.get(MANIFEST, b"{}")) + for field, value in expected.items(): + if actual.get(field) != value: + raise ValueError(f"npm metadata mismatch: {field}") + for name in ("LICENSE", "NOTICE", "DISCLAIMER", "README.md"): + if files.get(name) != (root / name).read_bytes(): + raise ValueError(f"npm legal/display content differs: {name}") + for name, data in files.items(): + source = root / name + if not source.is_file() or source.read_bytes() != data: + raise ValueError(f"Unexpected or changed npm file: {name}") + for directory in ("lib",): + for source in (root / directory).rglob("*"): + if source.is_file() and files.get(source.relative_to(root).as_posix()) != source.read_bytes(): + raise ValueError(f"npm package omits built/source file: {source}") + for field in ("main", "types"): + if expected.get(field) and expected[field].removeprefix("./") not in files: + raise ValueError(f"npm entry point missing: {field}") + print(f"npm contents verified against approved source/build: {path}") + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("command", choices=["meta", "archive", "verify", "npm-check"]) + parser.add_argument("tag") + parser.add_argument("output", nargs="?", default="dist-release") + args = parser.parse_args() + if args.command == "npm-check": + check_npm(args.tag) + return + version, rc, name = metadata(args.tag) + if args.command == "meta": + print(f"version={version}\nis_rc={str(rc).lower()}\nname={name}") + return + output = Path(args.output) + if output.exists() and any(output.iterdir()): + raise ValueError("Refusing to replace existing release files; choose an empty output directory") + output.mkdir(parents=True, exist_ok=True) + expected = source(args.tag, name) + filename = name + ".tar.gz" + target = output / filename + if args.command == "archive": + if not rc: + raise ValueError("Final releases must reuse approved ASF originals, not rebuild source archives") + target.write_bytes(gzip.compress(expected, mtime=0)) + (output / (filename + ".sha512")).write_text(hashlib.sha512(target.read_bytes()).hexdigest() + " " + filename + "\n", encoding="ascii") + else: + if rc: + raise ValueError("An RC cannot enter final publication") + url = f"{ASF_BASE}/{COMPONENT}-{version}-incubating/{filename}" + for suffix in ("", ".sha512", ".asc"): + (output / (filename + suffix)).write_bytes(download(url + suffix)) + checksum = (output / (filename + ".sha512")).read_text().split() + if len(checksum) != 2 or checksum[1].lstrip("*") != filename: + raise ValueError("Unexpected checksum record") + if hashlib.sha512(target.read_bytes()).hexdigest() != checksum[0].lower(): + raise ValueError("SHA-512 mismatch") + verify_signature(download(ASF_BASE + "/KEYS"), target, output / (filename + ".asc")) + if inventory(target.read_bytes(), name) != inventory(expected, name): + raise ValueError("Approved source contents/modes differ from the selected Git tag") + # Build package registries only from the approved source, never a fresh checkout. + with tarfile.open(target) as archive: + archive.extractall(output / "source", filter="data") + print(f"Verified {target}; {'local unsigned candidate' if rc else 'approved ASF original'}") + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/test_release.py b/.github/scripts/test_release.py new file mode 100644 index 0000000..d2ab77a --- /dev/null +++ b/.github/scripts/test_release.py @@ -0,0 +1,205 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import contextlib +import gzip +import hashlib +import io +import os +import subprocess +import sys +import tarfile +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +import release + + +class ReleaseSafetyTest(unittest.TestCase): + def setUp(self): + self.previous = Path.cwd() + self.directory = tempfile.TemporaryDirectory() + os.chdir(self.directory.name) + subprocess.run(["git", "init", "-q"], check=True) + for key, value in (("user.name", "Fixture"), ("user.email", "[email protected]")): + subprocess.run(["git", "config", key, value], check=True) + text = ('{"name":"%s","version":"1.2.3"}\n' % release.EXPECTED_PACKAGE) if release.MANIFEST.endswith("json") else ('[project]\nname="%s"\nversion="1.2.3"\n' % release.EXPECTED_PACKAGE) + Path(release.MANIFEST).write_text(text, encoding="utf8") + for filename in ("LICENSE", "NOTICE", "DISCLAIMER", "README.md"): + Path(filename).write_text("Fixture for command safety, not a legal compliance assessment.\n", encoding="utf8") + self.commit() + for tag in ("v1.2.3", "v1.2.3-rc1"): + subprocess.run(["git", "tag", tag], check=True) + self.name = release.metadata("v1.2.3")[2] + # A different gzip compression level proves verify preserves originals. + self.original = gzip.compress(release.source("v1.2.3", self.name), compresslevel=1, mtime=0) + + def tearDown(self): + os.chdir(self.previous) + self.directory.cleanup() + + def commit(self): + subprocess.run(["git", "add", "-A"], check=True) + subprocess.run(["git", "commit", "-qm", "fixture"], check=True) + + def invoke(self, *args): + with patch.object(sys, "argv", ["release.py", *args]), contextlib.redirect_stdout(io.StringIO()): + release.main() + + def downloaded(self, url): + if url.endswith("KEYS"): + return b"MOCK KEY: cryptographic verification is covered by later real RC checks" + if url.endswith(".sha512"): + return (hashlib.sha512(self.original).hexdigest() + " " + self.name + ".tar.gz\n").encode() + if url.endswith(".asc"): + return b"MOCK SIGNATURE" + return self.original + + def test_rejects_legacy_package_identity(self): + manifest = Path(release.MANIFEST) + manifest.write_text(manifest.read_text().replace(release.EXPECTED_PACKAGE, "legacy-package"), encoding="utf8") + self.commit() + subprocess.run(["git", "tag", "v1.2.3-rc9"], check=True) + with self.assertRaisesRegex(ValueError, "identity"): + release.metadata("v1.2.3-rc9") + + def test_strict_tags(self): + for tag in ("master", "v1", "v1.2.3-rc0", "v1.2.3-beta1", "v1.2.3-rc1/x", "v1.2.3 "): + with self.subTest(tag=tag), self.assertRaises(ValueError): + release.metadata(tag) + + def test_version_must_be_committed(self): + subprocess.run(["git", "tag", "v1.2.4-rc1"], check=True) + with self.assertRaises(ValueError): + release.metadata("v1.2.4-rc1") + + def test_reproducible_candidate(self): + self.invoke("archive", "v1.2.3-rc1", "one") + self.invoke("archive", "v1.2.3-rc1", "two") + filename = self.name + ".tar.gz" + self.assertEqual((Path("one") / filename).read_bytes(), (Path("two") / filename).read_bytes()) + + def test_refuses_overwrite(self): + Path("existing").mkdir() + Path("existing/sentinel").write_bytes(b"preserve") + with self.assertRaises(ValueError): + self.invoke("archive", "v1.2.3-rc1", "existing") + self.assertEqual(Path("existing/sentinel").read_bytes(), b"preserve") + + def test_final_cannot_be_rebuilt(self): + with self.assertRaises(ValueError): + self.invoke("archive", "v1.2.3", "final") + + def test_rc_cannot_be_promoted(self): + with self.assertRaises(ValueError), patch.object(release, "download") as download: + self.invoke("verify", "v1.2.3-rc1", "promotion") + download.assert_not_called() + + def test_missing_legal_file(self): + Path("NOTICE").unlink() + self.commit() + subprocess.run(["git", "tag", "v1.2.3-rc2"], check=True) + with self.assertRaises(ValueError): + self.invoke("archive", "v1.2.3-rc2", "missing") + + def test_rejects_unsafe_archive_entries(self): + for path, link in (("../escape", False), (self.name + "/../escape", False), (self.name + "/link", True)): + buffer = io.BytesIO() + with tarfile.open(fileobj=buffer, mode="w") as archive: + entry = tarfile.TarInfo(path) + if link: + entry.type = tarfile.SYMTYPE + entry.linkname = "../../escape" + archive.addfile(entry) + with self.subTest(path=path), self.assertRaises(ValueError): + release.inventory(buffer.getvalue(), self.name) + + def test_preserves_original_bytes_after_mock_verification(self): + with patch.object(release, "download", side_effect=self.downloaded), patch.object(release, "verify_signature") as gpg: + self.invoke("verify", "v1.2.3", "approved") + self.assertEqual(gpg.call_count, 1) + self.assertEqual((Path("approved") / (self.name + ".tar.gz")).read_bytes(), self.original) + self.assertTrue((Path("approved/source") / self.name / release.MANIFEST).is_file()) + + def test_checksum_failure_stops_before_gpg_and_build(self): + def bad(url): + return ("0" * 128 + " " + self.name + ".tar.gz\n").encode() if url.endswith(".sha512") else self.downloaded(url) + + with patch.object(release, "download", side_effect=bad), patch.object(release, "verify_signature") as gpg: + with self.assertRaises(ValueError): + self.invoke("verify", "v1.2.3", "bad-checksum") + gpg.assert_not_called() + self.assertFalse(Path("bad-checksum/source").exists()) + + def test_signature_failure_stops_before_build(self): + with patch.object(release, "download", side_effect=self.downloaded): + with patch.object(release, "verify_signature", side_effect=subprocess.CalledProcessError(1, "gpg")): + with self.assertRaises(subprocess.CalledProcessError): + self.invoke("verify", "v1.2.3", "bad-signature") + self.assertFalse(Path("bad-signature/source").exists()) + + def test_different_approved_source_is_rejected(self): + Path("README.md").write_text("Changed source\n", encoding="utf8") + self.commit() + subprocess.run(["git", "tag", "-f", "v1.2.3"], check=True, stdout=subprocess.DEVNULL) + with patch.object(release, "download", side_effect=self.downloaded), patch.object(release, "verify_signature"): + with self.assertRaises(ValueError): + self.invoke("verify", "v1.2.3", "different-source") + self.assertFalse(Path("different-source/source").exists()) + + def npm_fixture(self, change=None): + Path("lib").mkdir(exist_ok=True) + Path("lib/index.js").write_bytes(b"exports.fixture = true;\n") + Path(release.MANIFEST).write_text('{"name":"fixture","version":"1.2.3","license":"Apache-2.0","main":"lib/index.js"}', encoding="utf8") + files = {name: Path(name).read_bytes() for name in (release.MANIFEST,"LICENSE","NOTICE","DISCLAIMER","README.md","lib/index.js")} + if change: + change(files) + with tarfile.open("fixture.tgz", "w:gz") as archive: + for name, data in files.items(): + entry = tarfile.TarInfo("package/" + name) + entry.size = len(data) + archive.addfile(entry, io.BytesIO(data)) + + def test_npm_preserves_source_legal_and_entry_files(self): + self.npm_fixture() + release.check_npm("fixture.tgz") + + def test_npm_rejects_missing_built_file(self): + self.npm_fixture(lambda files: files.pop("lib/index.js")) + with self.assertRaises(ValueError): + release.check_npm("fixture.tgz") + + def test_npm_rejects_changed_dependency_metadata(self): + self.npm_fixture(lambda files: files.update({release.MANIFEST: b'{"name":"fixture","version":"9.9.9"}'})) + with self.assertRaises(ValueError): + release.check_npm("fixture.tgz") + + def test_npm_rejects_changed_legal_file(self): + self.npm_fixture(lambda files: files.update({"LICENSE":b"wrong"})) + with self.assertRaises(ValueError): + release.check_npm("fixture.tgz") + + def test_npm_rejects_path_traversal(self): + self.npm_fixture(lambda files: files.update({"../outside":b"unsafe"})) + with self.assertRaises(ValueError): + release.check_npm("fixture.tgz") + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ffe106..f8d7a3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,10 @@ jobs: cache: yarn - name: Install Dependency - run: yarn install --frozen-lockfile + run: yarn install --frozen-lockfile --ignore-scripts + + - name: Check release safety helpers + run: python3 -m unittest discover -s .github/scripts -p "test_*.py" - name: Run Lint run: yarn run lint @@ -47,7 +50,7 @@ jobs: cache: yarn - name: Install Dependency - run: yarn install --frozen-lockfile + run: yarn install --frozen-lockfile --ignore-scripts - name: Run Unit test run: yarn run test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f96b5a5..bf3c513 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -15,27 +15,32 @@ # specific language governing permissions and limitations # under the License. -# Manual release. Releases are cut by pushing a tag by hand: -# -# vX.Y.Z-rcN -> GitHub *pre-release* only (the candidate to vote on) -# vX.Y.Z -> GitHub release + publish to npm (after the vote passed) -# -# Both cases attach the same Apache source package. The release manager -# downloads it, signs it locally and takes it to the vote. - -name: release +name: Release on: push: - tags: - - 'v*' + tags: ['v*-rc*'] + workflow_dispatch: + inputs: + tag: + description: 'Existing final tag vX.Y.Z, pointing at the approved candidate commit' + required: true + type: string + project_result: + description: 'Public project vote result URL (lists.apache.org/thread/...)' + required: true + type: string + incubator_result: + description: 'Public IPMC vote result URL (lists.apache.org/thread/...)' + required: true + type: string permissions: contents: write -env: - # Base name of the Apache source package, without version/suffix. - ARTIFACT_PREFIX: apache-casbin-js +concurrency: + group: release-${{ github.repository }}-${{ inputs.tag || github.ref_name }} + cancel-in-progress: false jobs: release: @@ -44,63 +49,95 @@ jobs: steps: - uses: actions/checkout@v4 with: + ref: ${{ inputs.tag || github.ref }} fetch-depth: 0 - - - name: Resolve version from tag + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Validate mode and committed version + id: meta + env: + TAG: ${{ inputs.tag || github.ref_name }} + EVENT: ${{ github.event_name }} + PROJECT_RESULT: ${{ inputs.project_result }} + INCUBATOR_RESULT: ${{ inputs.incubator_result }} run: | set -euo pipefail - TAG="${GITHUB_REF_NAME}" - if [[ ! "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-rc[0-9]+)?$ ]]; then - echo "::error::Tag '${TAG}' is not a release tag. Expected vX.Y.Z or vX.Y.Z-rcN." - exit 1 + python .github/scripts/release.py meta "$TAG" >> "$GITHUB_OUTPUT" + if [[ "$EVENT" == workflow_dispatch ]]; then + [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] + [[ "$PROJECT_RESULT" =~ ^https://lists\.apache\.org/thread/[a-z0-9]+$ ]] + [[ "$INCUBATOR_RESULT" =~ ^https://lists\.apache\.org/thread/[a-z0-9]+$ ]] + else + [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc[1-9][0-9]*$ ]] fi - FULL="${TAG#v}" - # The release candidate and the final release are the very same - # artifact, so the -rcN suffix never appears in the package name. - VERSION="${FULL%-rc*}" - if [[ "${FULL}" == *-rc* ]]; then PRERELEASE=true; else PRERELEASE=false; fi - echo "VERSION=${VERSION}" >> "${GITHUB_ENV}" - echo "PRERELEASE=${PRERELEASE}" >> "${GITHUB_ENV}" - echo "SRC_NAME=${ARTIFACT_PREFIX}-${VERSION}-incubating-src" >> "${GITHUB_ENV}" - - - name: Build Apache source package + - name: Refuse replacement of an existing GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ inputs.tag || github.ref_name }} run: | set -euo pipefail - git archive --format=tar.gz --prefix="${SRC_NAME}/" -o "${SRC_NAME}.tar.gz" HEAD - sha512sum "${SRC_NAME}.tar.gz" > "${SRC_NAME}.tar.gz.sha512" - - - name: Set up Node.js - uses: actions/setup-node@v4 + if gh release view "$TAG" >/dev/null 2>&1; then + echo '::error::Release exists; assets will not be overwritten. Inspect partial publication before retrying.' + exit 1 + fi + - name: Export unsigned source candidate + if: steps.meta.outputs.is_rc == 'true' + env: + TAG: ${{ github.ref_name }} + run: python .github/scripts/release.py archive "$TAG" + - name: Verify approved ASF originals against the final tag + if: steps.meta.outputs.is_rc == 'false' + env: + TAG: ${{ inputs.tag }} + run: python .github/scripts/release.py verify "$TAG" + - uses: actions/setup-node@v4 + if: steps.meta.outputs.is_rc == 'false' with: - node-version: '20.x' + node-version: '22' registry-url: 'https://registry.npmjs.org' - - - name: Install Dependency - run: yarn install - - - name: Run Unit test - run: yarn run test - - # An -rcN tag only ever produces a pre-release; the vote decides whether - # it is promoted, which happens by pushing the matching vX.Y.Z tag. - - name: Publish GitHub release + - name: Build and test npm package from approved source + if: steps.meta.outputs.is_rc == 'false' env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NAME: ${{ steps.meta.outputs.name }} run: | set -euo pipefail - FLAGS="" - if [[ "${PRERELEASE}" == "true" ]]; then FLAGS="--prerelease"; fi - gh release create "${GITHUB_REF_NAME}" \ - --title "${GITHUB_REF_NAME}" \ - --generate-notes \ - ${FLAGS} \ - "${SRC_NAME}.tar.gz" "${SRC_NAME}.tar.gz.sha512" - - - name: Publish to npm - if: env.PRERELEASE == 'false' + cd "dist-release/source/$NAME" + yarn install --frozen-lockfile --ignore-scripts + yarn lint + yarn format:check + yarn test --runInBand + yarn build + mkdir -p "$GITHUB_WORKSPACE/npm-package" + npm pack --ignore-scripts --pack-destination "$GITHUB_WORKSPACE/npm-package" + python "$GITHUB_WORKSPACE/.github/scripts/release.py" npm-check "$GITHUB_WORKSPACE"/npm-package/*.tgz + - name: Publish approved-source npm package + if: steps.meta.outputs.is_rc == 'false' env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish npm-package/*.tgz --access public + + - name: Write release notes + env: + IS_RC: ${{ steps.meta.outputs.is_rc }} + VERSION: ${{ steps.meta.outputs.version }} + PROJECT_RESULT: ${{ inputs.project_result }} + INCUBATOR_RESULT: ${{ inputs.incubator_result }} + run: | + printf 'Apache Casbin (Incubating) — Casbin.js %s\n\n' "$VERSION" > release-notes.md + cat DISCLAIMER >> release-notes.md + if [[ "$IS_RC" == true ]]; then + printf '\n\nUnsigned source candidate for review; the RM must sign and stage it before voting. This is not an approved release.\n' >> release-notes.md + else + printf '\n\nProject result: %s\nIPMC result: %s\nSource assets are the approved ASF originals.\n' "$PROJECT_RESULT" "$INCUBATOR_RESULT" >> release-notes.md + fi + - name: Create GitHub release without replacing assets + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ inputs.tag || github.ref_name }} + IS_RC: ${{ steps.meta.outputs.is_rc }} run: | set -euo pipefail - npm version "${VERSION}" --no-git-tag-version --allow-same-version - npm publish --access public + kind=(--latest) + if [[ "$IS_RC" == true ]]; then kind=(--prerelease --latest=false); fi + gh release create "$TAG" --title "$TAG (Apache Casbin Incubating)" --notes-file release-notes.md "${kind[@]}" dist-release/*.tar.gz dist-release/*.sha512 $(find dist-release -maxdepth 1 -name '*.asc' -print) --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
