mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-22 19:42:13 +00:00
ci: package releases with drafts in simplex-chat-libs (#7754)
* ci: package releases with drafts in simplex-chat-libs * test release * core: 7.1.0.12 * packages: release nodejs and python libraries * fixes --------- Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
This commit is contained in:
1 parent
0563460204
commit
a766d0af0f
14 files changed
+1797
-559
No files matched your search
+356
-82
@@ -33,6 +33,7 @@ env:
|
||||
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
branch: ${{ github.base_ref || github.ref_name }}
|
||||
cache_o0: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/stable') }}
|
||||
release_branch: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/stable') && github.ref_name || '' }}
|
||||
ZSTD_CLEVEL: 9
|
||||
|
||||
# This workflow uses custom actions (prepare-build and prepare-release) defined in:
|
||||
@@ -74,7 +75,8 @@ jobs:
|
||||
# =============================
|
||||
|
||||
# Create release, but only if it's triggered by tag push.
|
||||
# Create simplex-chat-libs release core-<version>, but only if the head commit of a push to master or stable changes the version in simplex-chat.cabal.
|
||||
# On pushes to master or stable: claim the simplex-chat-libs release core-<version of simplex-chat.cabal> for the branch
|
||||
# and create its draft, create the draft of a missing add-on release, and find the package versions to publish.
|
||||
# On pull requests and other pushes, this job will always complete.
|
||||
|
||||
# Secrets:
|
||||
@@ -87,8 +89,15 @@ jobs:
|
||||
maybe-release:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
libs_tag: ${{ steps.libs_commit.outputs.tag }}
|
||||
libs_tag: ${{ steps.libs.outputs.tag }}
|
||||
core_changed: ${{ steps.core.outputs.core }}
|
||||
addon_tag: ${{ steps.addon.outputs.tag }}
|
||||
addon_draft: ${{ steps.addon.outputs.draft }}
|
||||
libs_latest: ${{ steps.latest_libs.outputs.tag }}
|
||||
publish_types: ${{ steps.libs.outputs.tag && steps.publish.outputs.types_version || '' }}
|
||||
publish_nodejs: ${{ steps.publish.outputs.nodejs_version }}
|
||||
publish_python: ${{ steps.publish.outputs.python_version }}
|
||||
publish: ${{ steps.publish.outputs.nodejs_version || steps.publish.outputs.python_version }}
|
||||
steps:
|
||||
- name: Clone project
|
||||
if: github.event_name == 'push' || github.event_name == 'pull_request'
|
||||
@@ -129,38 +138,68 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check version change
|
||||
id: check_version
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/stable'
|
||||
- name: Library release
|
||||
id: libs
|
||||
if: env.release_branch
|
||||
shell: bash
|
||||
run: |
|
||||
version=$(sed -n 's/^version: *//p' simplex-chat.cabal)
|
||||
prev_version=$(git show HEAD^1:simplex-chat.cabal | sed -n 's/^version: *//p')
|
||||
if [ "$version" != "$prev_version" ]; then
|
||||
echo "version=$version" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Record libs release
|
||||
id: libs_commit
|
||||
if: steps.check_version.outputs.version
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.NODEJS_REPO_TOKEN }}
|
||||
LIBS_REPOSITORY: ${{ github.repository }}-libs
|
||||
VERSION: ${{ steps.check_version.outputs.version }}
|
||||
run: |
|
||||
commit=$(python3 scripts/ci/packages_release.py record --core "$VERSION" --commit "$GITHUB_SHA")
|
||||
echo "tag=core-$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "commit=$commit" >> "$GITHUB_OUTPUT"
|
||||
version=$(sed -n 's/^version: *//p' simplex-chat.cabal)
|
||||
tag=core-$version
|
||||
claim_output=$(python3 scripts/ci/packages_release.py claim-core "$version" "$GITHUB_SHA" "$release_branch")
|
||||
eval "$claim_output"
|
||||
if [ "$claim" = skip ]; then
|
||||
echo "::notice title=Library release::core $version is recorded for another commit"
|
||||
exit 0
|
||||
fi
|
||||
status_output=$(python3 scripts/ci/packages_release.py release-status "$tag")
|
||||
eval "$status_output"
|
||||
if [ "$status" = absent ]; then
|
||||
gh release create "$tag" --repo "$LIBS_REPOSITORY" --draft --title "$tag" --notes "" ${commit:+--target "$commit"}
|
||||
fi
|
||||
if [ "$status" != published ]; then
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# creates the tag on the commit that recorded the release
|
||||
- name: Create libs release
|
||||
if: steps.check_version.outputs.version
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: ${{ github.repository }}-libs
|
||||
tag_name: ${{ steps.libs_commit.outputs.tag }}
|
||||
target_commitish: ${{ steps.libs_commit.outputs.commit }}
|
||||
token: ${{ secrets.NODEJS_REPO_TOKEN }}
|
||||
- name: Add-on release
|
||||
id: addon
|
||||
if: env.release_branch
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.NODEJS_REPO_TOKEN }}
|
||||
LIBS_REPOSITORY: ${{ github.repository }}-libs
|
||||
run: |
|
||||
addon_output=$(python3 scripts/ci/packages_release.py addon-release)
|
||||
eval "$addon_output"
|
||||
if [ "$status" = absent ]; then
|
||||
tag=nodejs-addon-${GITHUB_SHA::7}
|
||||
gh release create "$tag" --repo "$LIBS_REPOSITORY" --draft --title "$tag" --notes "$notes"
|
||||
fi
|
||||
if [ "$status" != published ]; then
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
echo "draft=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Latest library
|
||||
id: latest_libs
|
||||
if: steps.addon.outputs.draft
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.NODEJS_REPO_TOKEN }}
|
||||
LIBS_REPOSITORY: ${{ github.repository }}-libs
|
||||
run: python3 scripts/ci/packages_release.py latest-libs --branch "$release_branch" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Versions to publish
|
||||
id: publish
|
||||
if: env.release_branch
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
LIBS_REPOSITORY: ${{ github.repository }}-libs
|
||||
NPM_SCOPE: ${{ vars.NPM_SCOPE }}
|
||||
PYPI_PROJECT: ${{ vars.PYPI_PROJECT }}
|
||||
PYPI_URL: ${{ vars.PYPI_URL }}
|
||||
run: python3 scripts/ci/packages_release.py publish-plan >> "$GITHUB_OUTPUT"
|
||||
|
||||
# =========================
|
||||
# Linux Build
|
||||
@@ -444,12 +483,11 @@ jobs:
|
||||
|
||||
- name: Upload library
|
||||
if: needs.maybe-release.outputs.libs_tag && matrix.libs
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: ${{ github.repository }}-libs
|
||||
tag_name: ${{ needs.maybe-release.outputs.libs_tag }}
|
||||
files: release-libs/*.zip
|
||||
token: ${{ secrets.NODEJS_REPO_TOKEN }}
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.NODEJS_REPO_TOKEN }}
|
||||
TAG: ${{ needs.maybe-release.outputs.libs_tag }}
|
||||
run: gh release upload "$TAG" release-libs/*.zip --clobber --repo "${{ github.repository }}-libs"
|
||||
|
||||
- name: Build CLI deb
|
||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.should_run == true
|
||||
@@ -591,7 +629,7 @@ jobs:
|
||||
|
||||
- name: Setup Node.js
|
||||
id: setup-node
|
||||
if: ${{ !cancelled() && needs.maybe-release.outputs.core_changed && steps.build-lib.outcome == 'success' }}
|
||||
if: ${{ !cancelled() && ((needs.maybe-release.outputs.core_changed && steps.build-lib.outcome == 'success') || (matrix.libs && (needs.maybe-release.outputs.addon_draft || needs.maybe-release.outputs.publish))) }}
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 22
|
||||
@@ -603,35 +641,42 @@ jobs:
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Build simplex-chat (npm) and add-on
|
||||
id: build-nodejs
|
||||
if: ${{ !cancelled() && steps.setup-python.outcome == 'success' }}
|
||||
shell: bash
|
||||
working-directory: packages/simplex-chat-nodejs
|
||||
run: |
|
||||
(cd ../simplex-chat-client/types/typescript && npm install --no-audit --no-fund && npx tsc)
|
||||
npm install --ignore-scripts --no-save --no-audit --no-fund ../simplex-chat-client/types/typescript
|
||||
npx --yes node-gyp rebuild
|
||||
npx tsc && cp src/simplex.* dist
|
||||
|
||||
- name: Test simplex-chat (npm)
|
||||
id: test-nodejs
|
||||
if: ${{ !cancelled() && steps.build-nodejs.outcome == 'success' }}
|
||||
- name: Package tests (pull request)
|
||||
if: ${{ !cancelled() && steps.setup-python.outcome == 'success' && needs.maybe-release.outputs.core_changed }}
|
||||
timeout-minutes: 30
|
||||
shell: bash
|
||||
working-directory: packages/simplex-chat-nodejs
|
||||
env:
|
||||
SIMPLEX_LIBS_DIR: ${{ github.workspace }}/release-libs/simplex-chat-libs-linux-${{ matrix.arch }}/libs
|
||||
run: npx jest --forceExit --testNamePattern '^(?!.*use preset servers)'
|
||||
run: |
|
||||
scripts/ci/package-tests.sh build linux-${{ matrix.arch }}
|
||||
scripts/ci/package-tests.sh test
|
||||
|
||||
- name: Test simplex-chat (PyPI)
|
||||
if: ${{ !cancelled() && steps.test-nodejs.outcome == 'success' }}
|
||||
- name: Add-on
|
||||
if: ${{ !cancelled() && steps.setup-python.outcome == 'success' && needs.maybe-release.outputs.addon_draft }}
|
||||
timeout-minutes: 30
|
||||
shell: bash
|
||||
env:
|
||||
LIBS_RELEASE: ${{ needs.maybe-release.outputs.libs_latest }}
|
||||
ADDON_TAG: ${{ needs.maybe-release.outputs.addon_tag }}
|
||||
LIBS_REPOSITORY: ${{ github.repository }}-libs
|
||||
GH_TOKEN: ${{ secrets.NODEJS_REPO_TOKEN }}
|
||||
run: scripts/ci/package-tests.sh addon linux-${{ matrix.arch }}
|
||||
|
||||
- name: Package tests
|
||||
id: package-tests
|
||||
if: ${{ !cancelled() && steps.setup-python.outcome == 'success' && needs.maybe-release.outputs.publish }}
|
||||
timeout-minutes: 30
|
||||
shell: bash
|
||||
working-directory: packages/simplex-chat-python
|
||||
run: |
|
||||
python -m pip install -e ".[test]"
|
||||
python -m pytest -q
|
||||
scripts/ci/package-tests.sh publish linux-${{ matrix.arch }} ${{ needs.maybe-release.outputs.publish_nodejs && 'nodejs' || '' }} ${{ needs.maybe-release.outputs.publish_python && 'python' || '' }}
|
||||
echo "linux-${{ matrix.arch }}" > "$RUNNER_TEMP/package-tests"
|
||||
|
||||
- name: Package tests result
|
||||
if: ${{ !cancelled() && steps.package-tests.outcome == 'success' }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: package-tests-linux-${{ matrix.arch }}
|
||||
path: ${{ runner.temp }}/package-tests
|
||||
|
||||
- name: Remove -O2 build directory
|
||||
id: remove-o2
|
||||
@@ -821,12 +866,11 @@ jobs:
|
||||
(cd postgres-libs && zip -r simplex-chat-libs-linux-x86_64-postgres.zip libs)
|
||||
|
||||
- name: Upload postgres library
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
repository: ${{ github.repository }}-libs
|
||||
tag_name: ${{ needs.maybe-release.outputs.libs_tag }}
|
||||
files: postgres-libs/*.zip
|
||||
token: ${{ secrets.NODEJS_REPO_TOKEN }}
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.NODEJS_REPO_TOKEN }}
|
||||
TAG: ${{ needs.maybe-release.outputs.libs_tag }}
|
||||
run: gh release upload "$TAG" postgres-libs/*.zip --clobber --repo "${{ github.repository }}-libs"
|
||||
|
||||
# =========================
|
||||
# MacOS Build
|
||||
@@ -1084,12 +1128,52 @@ jobs:
|
||||
|
||||
- name: Upload library
|
||||
if: ${{ !cancelled() && steps.mac_lib_build.outcome == 'success' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.NODEJS_REPO_TOKEN }}
|
||||
TAG: ${{ needs.maybe-release.outputs.libs_tag }}
|
||||
run: gh release upload "$TAG" release-libs/*.zip --clobber --repo "${{ github.repository }}-libs"
|
||||
|
||||
- name: Setup Node.js
|
||||
id: setup-node
|
||||
if: ${{ !cancelled() && (needs.maybe-release.outputs.addon_draft || needs.maybe-release.outputs.publish) }}
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
repository: ${{ github.repository }}-libs
|
||||
tag_name: ${{ needs.maybe-release.outputs.libs_tag }}
|
||||
files: release-libs/*.zip
|
||||
token: ${{ secrets.NODEJS_REPO_TOKEN }}
|
||||
node-version: 22
|
||||
|
||||
- name: Setup Python
|
||||
id: setup-python
|
||||
if: ${{ !cancelled() && steps.setup-node.outcome == 'success' }}
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Add-on
|
||||
if: ${{ !cancelled() && steps.setup-python.outcome == 'success' && needs.maybe-release.outputs.addon_draft }}
|
||||
timeout-minutes: 30
|
||||
shell: bash
|
||||
env:
|
||||
LIBS_RELEASE: ${{ needs.maybe-release.outputs.libs_latest }}
|
||||
ADDON_TAG: ${{ needs.maybe-release.outputs.addon_tag }}
|
||||
LIBS_REPOSITORY: ${{ github.repository }}-libs
|
||||
GH_TOKEN: ${{ secrets.NODEJS_REPO_TOKEN }}
|
||||
run: scripts/ci/package-tests.sh addon macos-${{ matrix.arch }}
|
||||
|
||||
- name: Package tests
|
||||
id: package-tests
|
||||
if: ${{ !cancelled() && steps.setup-python.outcome == 'success' && needs.maybe-release.outputs.publish }}
|
||||
timeout-minutes: 30
|
||||
shell: bash
|
||||
run: |
|
||||
scripts/ci/package-tests.sh publish macos-${{ matrix.arch }} ${{ needs.maybe-release.outputs.publish_nodejs && 'nodejs' || '' }} ${{ needs.maybe-release.outputs.publish_python && 'python' || '' }}
|
||||
echo "macos-${{ matrix.arch }}" > "$RUNNER_TEMP/package-tests"
|
||||
|
||||
- name: Package tests result
|
||||
if: ${{ !cancelled() && steps.package-tests.outcome == 'success' }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: package-tests-macos-${{ matrix.arch }}
|
||||
path: ${{ runner.temp }}/package-tests
|
||||
|
||||
- name: Remove -O2 build directory
|
||||
id: remove-o2
|
||||
@@ -1305,12 +1389,52 @@ jobs:
|
||||
|
||||
- name: Upload library
|
||||
if: needs.maybe-release.outputs.libs_tag
|
||||
uses: softprops/action-gh-release@v2
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.NODEJS_REPO_TOKEN }}
|
||||
TAG: ${{ needs.maybe-release.outputs.libs_tag }}
|
||||
run: gh release upload "$TAG" release-libs/*.zip --clobber --repo "${{ github.repository }}-libs"
|
||||
|
||||
- name: Setup Node.js
|
||||
id: setup-node
|
||||
if: ${{ !cancelled() && (needs.maybe-release.outputs.addon_draft || needs.maybe-release.outputs.publish) }}
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
repository: ${{ github.repository }}-libs
|
||||
tag_name: ${{ needs.maybe-release.outputs.libs_tag }}
|
||||
files: release-libs/*.zip
|
||||
token: ${{ secrets.NODEJS_REPO_TOKEN }}
|
||||
node-version: 22
|
||||
|
||||
- name: Setup Python
|
||||
id: setup-python
|
||||
if: ${{ !cancelled() && steps.setup-node.outcome == 'success' }}
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Add-on
|
||||
if: ${{ !cancelled() && steps.setup-python.outcome == 'success' && needs.maybe-release.outputs.addon_draft }}
|
||||
timeout-minutes: 30
|
||||
shell: bash
|
||||
env:
|
||||
LIBS_RELEASE: ${{ needs.maybe-release.outputs.libs_latest }}
|
||||
ADDON_TAG: ${{ needs.maybe-release.outputs.addon_tag }}
|
||||
LIBS_REPOSITORY: ${{ github.repository }}-libs
|
||||
GH_TOKEN: ${{ secrets.NODEJS_REPO_TOKEN }}
|
||||
run: scripts/ci/package-tests.sh addon windows-x86_64
|
||||
|
||||
- name: Package tests
|
||||
id: package-tests
|
||||
if: ${{ !cancelled() && steps.setup-python.outcome == 'success' && needs.maybe-release.outputs.publish }}
|
||||
timeout-minutes: 30
|
||||
shell: bash
|
||||
run: |
|
||||
scripts/ci/package-tests.sh publish windows-x86_64 ${{ needs.maybe-release.outputs.publish_nodejs && 'nodejs' || '' }} ${{ needs.maybe-release.outputs.publish_python && 'python' || '' }}
|
||||
echo "windows-x86_64" > "$RUNNER_TEMP/package-tests"
|
||||
|
||||
- name: Package tests result
|
||||
if: ${{ !cancelled() && steps.package-tests.outcome == 'success' }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: package-tests-windows-x86_64
|
||||
path: ${{ runner.temp }}/package-tests
|
||||
|
||||
- name: Upload CLI
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
@@ -1387,19 +1511,169 @@ jobs:
|
||||
key: ${{ steps.keys.outputs.dist }}-O0-${{ env.branch }}-${{ github.sha }}
|
||||
|
||||
# =========================
|
||||
# Packages release
|
||||
# Releases and packages
|
||||
# =========================
|
||||
|
||||
# A new library release is tested with the Node.js and Python packages and updates them in a release PR.
|
||||
|
||||
packages:
|
||||
releases:
|
||||
needs: [maybe-release, build-linux, build-linux-push, build-linux-postgres, build-macos, build-windows]
|
||||
if: ${{ !cancelled() && needs.maybe-release.outputs.libs_tag }}
|
||||
# the called workflow can't be granted more than its caller, and its publish job needs these
|
||||
if: ${{ !cancelled() && (needs.maybe-release.outputs.libs_tag || needs.maybe-release.outputs.addon_draft) }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.NODEJS_REPO_TOKEN }}
|
||||
LIBS_REPOSITORY: ${{ github.repository }}-libs
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Publish library release
|
||||
if: needs.maybe-release.outputs.libs_tag
|
||||
run: python3 scripts/ci/packages_release.py publish-release "${{ needs.maybe-release.outputs.libs_tag }}"
|
||||
|
||||
- name: Publish add-on release
|
||||
if: ${{ !cancelled() && needs.maybe-release.outputs.addon_draft }}
|
||||
run: python3 scripts/ci/packages_release.py publish-release "${{ needs.maybe-release.outputs.addon_tag }}"
|
||||
|
||||
# Publishing uses trusted publishing (OIDC) for this workflow on npm and PyPI,
|
||||
# or the NPM_TOKEN and PYPI_API_TOKEN secrets when they are set.
|
||||
# Test repositories: variables NPM_SCOPE, PYPI_PROJECT and PYPI_URL / PYPI_UPLOAD_URL (TestPyPI)
|
||||
# publish under other names.
|
||||
|
||||
publish:
|
||||
needs: [maybe-release, build-linux, build-linux-push, build-macos, build-windows, releases]
|
||||
if: ${{ !cancelled() && (needs.maybe-release.outputs.publish || needs.maybe-release.outputs.publish_types) }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
uses: ./.github/workflows/packages.yml
|
||||
with:
|
||||
libs_release: ${{ needs.maybe-release.outputs.libs_tag }}
|
||||
secrets: inherit
|
||||
# set by a publish step after it succeeds, so a failed publish still reports the earlier ones
|
||||
outputs:
|
||||
types: ${{ steps.types.outputs.version }}
|
||||
nodejs: ${{ steps.nodejs.outputs.version }}
|
||||
python: ${{ steps.python.outputs.version }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_SCOPE: ${{ vars.NPM_SCOPE }}
|
||||
PYPI_PROJECT: ${{ vars.PYPI_PROJECT }}
|
||||
PYPI_URL: ${{ vars.PYPI_URL }}
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download package tests results
|
||||
if: needs.maybe-release.outputs.publish
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: package-tests-*
|
||||
path: ${{ runner.temp }}/package-tests
|
||||
|
||||
- name: Check package tests and pins
|
||||
if: needs.maybe-release.outputs.publish
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
LIBS_REPOSITORY: ${{ github.repository }}-libs
|
||||
run: |
|
||||
passed=$(find "$RUNNER_TEMP/package-tests" -mindepth 1 -maxdepth 1 -type d -name 'package-tests-*' | wc -l)
|
||||
if [ "$passed" -ne 5 ]; then
|
||||
echo "::error::package tests passed on $passed of 5 platforms"
|
||||
exit 1
|
||||
fi
|
||||
python3 scripts/ci/packages_release.py check-pins
|
||||
|
||||
- name: Versions to publish
|
||||
id: plan
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
LIBS_REPOSITORY: ${{ github.repository }}-libs
|
||||
run: python3 scripts/ci/packages_release.py publish-plan >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.plan.outputs.types_version || steps.plan.outputs.nodejs_version
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 24
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Setup Python
|
||||
if: steps.plan.outputs.python_version
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
# Before the test names: the build installs @simplex-chat/types from this repository.
|
||||
- name: Build and test simplex-chat (npm)
|
||||
if: steps.plan.outputs.nodejs_version
|
||||
working-directory: packages/simplex-chat-nodejs
|
||||
run: |
|
||||
(cd ../simplex-chat-client/types/typescript && npm install --no-audit --no-fund && npx tsc)
|
||||
npm install --ignore-scripts --no-save --no-audit --no-fund ../simplex-chat-client/types/typescript
|
||||
npx tsc && cp src/simplex.* dist
|
||||
npx jest tests/libs.test.ts tests/api.unit.test.ts
|
||||
|
||||
- name: Use test package names
|
||||
if: vars.NPM_SCOPE || vars.PYPI_PROJECT
|
||||
run: python3 scripts/ci/packages_release.py test-names
|
||||
|
||||
- name: Publish @simplex-chat/types
|
||||
id: types
|
||||
if: steps.plan.outputs.types_version && needs.maybe-release.outputs.publish_types && needs.releases.result == 'success'
|
||||
working-directory: packages/simplex-chat-client/types/typescript
|
||||
env:
|
||||
VERSION: ${{ steps.plan.outputs.types_version }}
|
||||
DIST_TAG: ${{ steps.plan.outputs.types_dist_tag }}
|
||||
run: |
|
||||
npm install --no-audit --no-fund
|
||||
npm publish --provenance --access public --tag "$DIST_TAG"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Publish simplex-chat (npm)
|
||||
id: nodejs
|
||||
if: steps.plan.outputs.nodejs_version
|
||||
working-directory: packages/simplex-chat-nodejs
|
||||
env:
|
||||
VERSION: ${{ steps.plan.outputs.nodejs_version }}
|
||||
DIST_TAG: ${{ steps.plan.outputs.nodejs_dist_tag }}
|
||||
run: |
|
||||
npm publish --provenance --access public --tag "$DIST_TAG"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build and test simplex-chat (PyPI)
|
||||
if: steps.plan.outputs.python_version
|
||||
working-directory: packages/simplex-chat-python
|
||||
run: |
|
||||
python -m pip install build -e ".[test]"
|
||||
python -m pytest -q
|
||||
python -m build
|
||||
|
||||
- name: Publish simplex-chat (PyPI)
|
||||
if: steps.plan.outputs.python_version
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: packages/simplex-chat-python/dist
|
||||
repository-url: ${{ vars.PYPI_UPLOAD_URL || 'https://upload.pypi.org/legacy/' }}
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
|
||||
- name: Report the published PyPI version
|
||||
id: python
|
||||
if: steps.plan.outputs.python_version
|
||||
env:
|
||||
VERSION: ${{ steps.plan.outputs.python_version }}
|
||||
run: echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
record:
|
||||
needs: publish
|
||||
if: ${{ !cancelled() && (needs.publish.outputs.types || needs.publish.outputs.nodejs || needs.publish.outputs.python) }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Record published versions
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.NODEJS_REPO_TOKEN }}
|
||||
LIBS_REPOSITORY: ${{ github.repository }}-libs
|
||||
TYPES: ${{ needs.publish.outputs.types }}
|
||||
NODEJS: ${{ needs.publish.outputs.nodejs }}
|
||||
PYTHON: ${{ needs.publish.outputs.python }}
|
||||
run: |
|
||||
python3 scripts/ci/packages_release.py record --commit "$GITHUB_SHA" \
|
||||
--types "$TYPES" --nodejs "$NODEJS" --python "$PYTHON"
|
||||
@@ -1,60 +1,6 @@
|
||||
name: packages
|
||||
|
||||
# Releases simplex-chat (npm, PyPI) and @simplex-chat/types (npm) from master and stable:
|
||||
# 1. A missing Node.js add-on release (nodejs-addon-<hash of its sources>) is built and uploaded to simplex-chat-libs.
|
||||
# 2. Versions that are not published yet are published, and recorded in releases.toml of simplex-chat-libs.
|
||||
# npm prerelease versions get dist-tag beta, others latest.
|
||||
# 3. The release PR packages-release/<branch> pins new library and add-on releases with their SHA-256
|
||||
# and bumps the versions of changed packages. Merging it publishes them (step 2).
|
||||
# Package tests run on every platform with the pinned libsimplex. build.yml runs them with the libsimplex it builds:
|
||||
# for pull requests that change the chat core (linux-x86_64), and for a new library release before its release PR.
|
||||
|
||||
# Publishing uses trusted publishing (OIDC) for this workflow on npm and PyPI,
|
||||
# or the NPM_TOKEN and PYPI_API_TOKEN secrets when they are set.
|
||||
# Test repositories: variables NPM_SCOPE, PYPI_PROJECT and PYPI_URL / PYPI_UPLOAD_URL (TestPyPI)
|
||||
# publish under other names.
|
||||
|
||||
# Secrets:
|
||||
# -------
|
||||
# NODEJS_REPO_TOKEN
|
||||
# Only select repositories: simplex-chat-libs
|
||||
# Permissions:
|
||||
# * Contents (Read and Write)
|
||||
# RELEASE_PR_TOKEN
|
||||
# Only select repositories: simplex-chat
|
||||
# Permissions:
|
||||
# * Contents (Read and Write)
|
||||
# * Pull requests (Read and Write)
|
||||
# A PR opened with GITHUB_TOKEN does not run the checks of other workflows.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
nodejs_version:
|
||||
description: "simplex-chat npm version, bumped when needed by default"
|
||||
required: false
|
||||
types_version:
|
||||
description: "@simplex-chat/types version, bumped when needed by default"
|
||||
required: false
|
||||
python_version:
|
||||
description: "simplex-chat PyPI version, bumped when needed by default"
|
||||
required: false
|
||||
# build.yml: a new library release
|
||||
workflow_call:
|
||||
inputs:
|
||||
libs_release:
|
||||
description: "test with this simplex-chat-libs release"
|
||||
type: string
|
||||
default: ""
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- stable
|
||||
paths:
|
||||
- "packages/simplex-chat-nodejs/**"
|
||||
- "packages/simplex-chat-client/types/typescript/**"
|
||||
- "packages/simplex-chat-python/**"
|
||||
- ".github/workflows/packages.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "packages/simplex-chat-nodejs/**"
|
||||
@@ -79,47 +25,19 @@ jobs:
|
||||
addon:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
tag: ${{ steps.tag.outputs.tag }}
|
||||
release_branch: ${{ steps.branch.outputs.release_branch }}
|
||||
missing: ${{ steps.release.outputs.missing }}
|
||||
platforms: ${{ steps.plan.outputs.platforms }}
|
||||
test: ${{ steps.plan.outputs.test }}
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Add-on release tag
|
||||
id: tag
|
||||
run: echo "tag=$(python3 scripts/ci/packages_release.py addon-tag)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Release branch
|
||||
id: branch
|
||||
if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/stable')
|
||||
run: echo "release_branch=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check the add-on release
|
||||
id: release
|
||||
if: steps.branch.outputs.release_branch
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.NODEJS_REPO_TOKEN }}
|
||||
LIBS_REPOSITORY: ${{ github.repository }}-libs
|
||||
TAG: ${{ steps.tag.outputs.tag }}
|
||||
run: |
|
||||
if ! gh release view "$TAG" --repo "$LIBS_REPOSITORY" > /dev/null 2>&1; then
|
||||
echo "missing=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Build plan
|
||||
id: plan
|
||||
env:
|
||||
EVENT: ${{ github.event_name }}
|
||||
BASE: ${{ github.event.pull_request.base.sha }}
|
||||
RELEASE_MISSING: ${{ steps.release.outputs.missing }}
|
||||
LIBS_RELEASE: ${{ inputs.libs_release }}
|
||||
run: |
|
||||
if [ -n "$BASE" ]; then git fetch -q --depth=1 origin "$BASE"; fi
|
||||
python3 scripts/ci/packages_release.py build-plan --event "$EVENT" --base "$BASE" \
|
||||
--release-missing "$RELEASE_MISSING" --libs-release "$LIBS_RELEASE" >> "$GITHUB_OUTPUT"
|
||||
git fetch -q --depth=1 origin "$BASE"
|
||||
python3 scripts/ci/packages_release.py build-plan --base "$BASE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
name: "build ${{ matrix.platform }}"
|
||||
@@ -134,7 +52,6 @@ jobs:
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
working-directory: packages/simplex-chat-nodejs
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
@@ -149,222 +66,9 @@ jobs:
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
# The local types: the dependency can be bumped to a version that is not published yet.
|
||||
- name: Build simplex-chat (npm) and add-on
|
||||
run: |
|
||||
(cd ../simplex-chat-client/types/typescript && npm install --no-audit --no-fund && npx tsc)
|
||||
npm install --ignore-scripts --no-save --no-audit --no-fund ../simplex-chat-client/types/typescript
|
||||
npx --yes node-gyp rebuild
|
||||
npx tsc && cp src/simplex.* dist
|
||||
cp build/Release/simplex.node build/simplex-chat-nodejs-${{ matrix.platform }}.node
|
||||
run: scripts/ci/package-tests.sh build ${{ matrix.platform }}
|
||||
|
||||
- name: Download libsimplex release
|
||||
if: needs.addon.outputs.test && inputs.libs_release
|
||||
env:
|
||||
URL: https://github.com/${{ github.repository }}-libs/releases/download/${{ inputs.libs_release }}/simplex-chat-libs-${{ matrix.platform }}.zip
|
||||
run: curl -fsSL --create-dirs -o "$RUNNER_TEMP/libsimplex/simplex-chat-libs-${{ matrix.platform }}.zip" "$URL"
|
||||
|
||||
- name: Unpack libsimplex
|
||||
if: needs.addon.outputs.test && inputs.libs_release
|
||||
run: python -m zipfile -e "$RUNNER_TEMP/libsimplex/simplex-chat-libs-${{ matrix.platform }}.zip" "$RUNNER_TEMP/libsimplex"
|
||||
|
||||
- name: Test simplex-chat (npm)
|
||||
- name: Test simplex-chat (npm and PyPI)
|
||||
if: needs.addon.outputs.test
|
||||
env:
|
||||
SIMPLEX_LIBS_DIR: ${{ inputs.libs_release && format('{0}/libsimplex/libs', runner.temp) || '' }}
|
||||
# A failed test can leave chat controllers running, and jest then waits for them.
|
||||
# Tests on the public preset servers fail in CI when a server is slow to connect.
|
||||
run: npx jest --forceExit --testNamePattern '^(?!.*use preset servers)'
|
||||
|
||||
- name: Test simplex-chat (PyPI)
|
||||
if: needs.addon.outputs.test
|
||||
working-directory: packages/simplex-chat-python
|
||||
run: |
|
||||
python -m pip install -e ".[test]"
|
||||
python -m pytest -q
|
||||
|
||||
- name: Upload add-on artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: simplex-nodejs-addon-${{ matrix.platform }}
|
||||
path: packages/simplex-chat-nodejs/build/simplex-chat-nodejs-${{ matrix.platform }}.node
|
||||
if-no-files-found: error
|
||||
|
||||
publish:
|
||||
needs: addon
|
||||
# Trusted publishing accepts this workflow only when it is not called by another one (build.yml).
|
||||
if: needs.addon.outputs.release_branch && contains(github.workflow_ref, '/.github/workflows/packages.yml@')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
# set by a publish step after it succeeds, so a failed publish still reports the earlier ones
|
||||
outputs:
|
||||
types: ${{ steps.types.outputs.version }}
|
||||
nodejs: ${{ steps.nodejs.outputs.version }}
|
||||
python: ${{ steps.python.outputs.version }}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Versions to publish
|
||||
id: plan
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
LIBS_REPOSITORY: ${{ github.repository }}-libs
|
||||
run: python3 scripts/ci/packages_release.py publish-plan >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.plan.outputs.types_version || steps.plan.outputs.nodejs_version
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 24
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Setup Python
|
||||
if: steps.plan.outputs.python_version
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
# Before the test names: the build installs @simplex-chat/types from this repository.
|
||||
- name: Build and test simplex-chat (npm)
|
||||
if: steps.plan.outputs.nodejs_version
|
||||
working-directory: packages/simplex-chat-nodejs
|
||||
run: |
|
||||
(cd ../simplex-chat-client/types/typescript && npm install --no-audit --no-fund && npx tsc)
|
||||
npm install --ignore-scripts --no-save --no-audit --no-fund ../simplex-chat-client/types/typescript
|
||||
npx tsc && cp src/simplex.* dist
|
||||
npx jest tests/libs.test.ts tests/api.unit.test.ts
|
||||
|
||||
- name: Use test package names
|
||||
if: vars.NPM_SCOPE || vars.PYPI_PROJECT
|
||||
run: python3 scripts/ci/packages_release.py test-names
|
||||
|
||||
- name: Publish @simplex-chat/types
|
||||
id: types
|
||||
if: steps.plan.outputs.types_version
|
||||
working-directory: packages/simplex-chat-client/types/typescript
|
||||
env:
|
||||
VERSION: ${{ steps.plan.outputs.types_version }}
|
||||
DIST_TAG: ${{ steps.plan.outputs.types_dist_tag }}
|
||||
run: |
|
||||
npm install --no-audit --no-fund
|
||||
npm publish --provenance --access public --tag "$DIST_TAG"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Publish simplex-chat (npm)
|
||||
id: nodejs
|
||||
if: steps.plan.outputs.nodejs_version
|
||||
working-directory: packages/simplex-chat-nodejs
|
||||
env:
|
||||
VERSION: ${{ steps.plan.outputs.nodejs_version }}
|
||||
DIST_TAG: ${{ steps.plan.outputs.nodejs_dist_tag }}
|
||||
run: |
|
||||
npm publish --provenance --access public --tag "$DIST_TAG"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build and test simplex-chat (PyPI)
|
||||
if: steps.plan.outputs.python_version
|
||||
working-directory: packages/simplex-chat-python
|
||||
run: |
|
||||
python -m pip install build -e ".[test]"
|
||||
python -m pytest -q
|
||||
python -m build
|
||||
|
||||
- name: Publish simplex-chat (PyPI)
|
||||
if: steps.plan.outputs.python_version
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: packages/simplex-chat-python/dist
|
||||
repository-url: ${{ vars.PYPI_UPLOAD_URL || 'https://upload.pypi.org/legacy/' }}
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
|
||||
- name: Report the published PyPI version
|
||||
id: python
|
||||
if: steps.plan.outputs.python_version
|
||||
env:
|
||||
VERSION: ${{ steps.plan.outputs.python_version }}
|
||||
run: echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# The only job here that writes to simplex-chat-libs; publish runs npm and pip installs without its token.
|
||||
record:
|
||||
needs: publish
|
||||
if: ${{ !cancelled() && (needs.publish.outputs.types || needs.publish.outputs.nodejs || needs.publish.outputs.python) }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Record published versions
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.NODEJS_REPO_TOKEN }}
|
||||
LIBS_REPOSITORY: ${{ github.repository }}-libs
|
||||
TYPES: ${{ needs.publish.outputs.types }}
|
||||
NODEJS: ${{ needs.publish.outputs.nodejs }}
|
||||
PYTHON: ${{ needs.publish.outputs.python }}
|
||||
run: |
|
||||
python3 scripts/ci/packages_release.py record --commit "$GITHUB_SHA" \
|
||||
--types "$TYPES" --nodejs "$NODEJS" --python "$PYTHON"
|
||||
|
||||
release:
|
||||
needs: [addon, build]
|
||||
# A merged release PR changes only versions and pins.
|
||||
if: >-
|
||||
!cancelled() && needs.addon.outputs.release_branch && needs.build.result != 'failure'
|
||||
&& !startsWith(github.event.head_commit.message, 'packages: release')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Outside the checkout: the release PR commits every change in it.
|
||||
- name: Download add-on artifacts
|
||||
if: needs.addon.outputs.missing
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: simplex-nodejs-addon-*
|
||||
merge-multiple: true
|
||||
path: ${{ runner.temp }}/addons
|
||||
|
||||
- name: Create add-on release
|
||||
if: needs.addon.outputs.missing
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
repository: ${{ github.repository }}-libs
|
||||
tag_name: ${{ needs.addon.outputs.tag }}
|
||||
files: ${{ runner.temp }}/addons/*.node
|
||||
token: ${{ secrets.NODEJS_REPO_TOKEN }}
|
||||
|
||||
- name: Update pins and versions
|
||||
id: prepare
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.NODEJS_REPO_TOKEN }}
|
||||
LIBS_REPOSITORY: ${{ github.repository }}-libs
|
||||
NODEJS_VERSION: ${{ inputs.nodejs_version }}
|
||||
TYPES_VERSION: ${{ inputs.types_version }}
|
||||
PYTHON_VERSION: ${{ inputs.python_version }}
|
||||
run: |
|
||||
python3 scripts/ci/packages_release.py prepare \
|
||||
--nodejs-version "$NODEJS_VERSION" \
|
||||
--types-version "$TYPES_VERSION" \
|
||||
--python-version "$PYTHON_VERSION" \
|
||||
--summary "$RUNNER_TEMP/summary.md"
|
||||
echo "title=$(head -n 1 "$RUNNER_TEMP/summary.md")" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create or update release PR
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_PR_TOKEN }}
|
||||
branch: packages-release/${{ github.ref_name }}
|
||||
base: ${{ github.ref_name }}
|
||||
title: ${{ steps.prepare.outputs.title }}
|
||||
commit-message: ${{ steps.prepare.outputs.title }}
|
||||
body-path: ${{ runner.temp }}/summary.md
|
||||
author: SimpleX Chat <build@simplex.chat>
|
||||
committer: SimpleX Chat <build@simplex.chat>
|
||||
delete-branch: true
|
||||
run: scripts/ci/package-tests.sh test
|
||||
@@ -0,0 +1,153 @@
|
||||
# Library and package releases
|
||||
|
||||
## Releases
|
||||
|
||||
| Release | Location | Version | Published on |
|
||||
|---|---|---|---|
|
||||
| libsimplex | simplex-chat-libs release `core-<version>` | `version` of `simplex-chat.cabal` | push to master or stable with a new core version |
|
||||
| Node.js add-on | simplex-chat-libs release `nodejs-addon-<7 characters of the commit>` | | push to master or stable with unreleased add-on sources |
|
||||
| `@simplex-chat/types` | npm | `version` of `packages/simplex-chat-client/types/typescript/package.json` | library release with an unpublished types version |
|
||||
| `simplex-chat` | npm | `version` of `packages/simplex-chat-nodejs/package.json` | push to master or stable with an unpublished version |
|
||||
| `simplex-chat` | PyPI | `__version__` of `packages/simplex-chat-python/src/simplex_chat/_version.py` | push to master or stable with an unpublished version |
|
||||
|
||||
Add-on sources:
|
||||
- `packages/simplex-chat-nodejs/binding.gyp`
|
||||
- `packages/simplex-chat-nodejs/cpp/`
|
||||
- `node-addon-api` version in `devDependencies` of `packages/simplex-chat-nodejs/package.json`
|
||||
|
||||
Release states in simplex-chat-libs:
|
||||
|
||||
```
|
||||
draft created by maybe-release; assets uploaded by the build jobs
|
||||
published every asset present; published by the releases job
|
||||
```
|
||||
|
||||
npm tag: `beta` for a version with a prerelease part, `latest` otherwise.
|
||||
|
||||
Releases and publications are independent of the Haskell test results.
|
||||
|
||||
`releases.toml` of simplex-chat-libs, written by `build.yml`:
|
||||
|
||||
```
|
||||
core.<branch>.<version> commit of the library release; branch: master or stable
|
||||
npm.<package>.<version> commit of the publication
|
||||
pypi.<package>.<version> commit of the publication
|
||||
```
|
||||
|
||||
## Library release
|
||||
|
||||
1. On master or stable, after `git pull`:
|
||||
|
||||
```
|
||||
python3 scripts/ci/packages_release.py core-release <version>
|
||||
```
|
||||
|
||||
Checked: version higher than the highest version recorded for the branch.
|
||||
|
||||
Written:
|
||||
- `version` of `simplex-chat.cabal`
|
||||
- with types changed since the previous library release of the branch: types `version` (bumped), `simplexChatCore`, line `SimpleX Chat core:` of the types README
|
||||
|
||||
`--types-version` sets the types version, for the first release of an app version, e.g. `--types-version 7.1.0-beta.7.1`.
|
||||
|
||||
2. Commit, push to the same branch.
|
||||
|
||||
3. Run of `build.yml`:
|
||||
- `maybe-release`: version claimed for the commit in `releases.toml`, draft `core-<version>` created
|
||||
- build jobs: 6 library zips uploaded
|
||||
- `releases`: release published
|
||||
- `publish`: `@simplex-chat/types` published, when its version is unpublished
|
||||
- `record`: types version recorded
|
||||
|
||||
Failed claim, with every build job skipped:
|
||||
- version lower than the highest version recorded for the branch
|
||||
- types changed, types version published
|
||||
- types changed, `simplexChatCore` different from the core version
|
||||
|
||||
After a failed claim, a fix commit is pushed; the version is claimed by the next push.
|
||||
|
||||
Incomplete draft: completed by a re-run of the failed jobs in the run of the claim. The version is skipped by later pushes, as it is recorded for another commit.
|
||||
|
||||
## Add-on release
|
||||
|
||||
Run of `build.yml` on a push to master or stable with unreleased add-on sources:
|
||||
- `maybe-release`: draft `nodejs-addon-<7 characters of the commit>` created, notes `sources: <hash of the add-on sources>`
|
||||
- build jobs, 5 platforms: add-on built, tested with the latest library release of the branch, uploaded
|
||||
- `releases`: release published
|
||||
|
||||
Incomplete draft: completed by a re-run or by the next push with the same add-on sources.
|
||||
|
||||
## Package release
|
||||
|
||||
1. On master or stable, after `git pull`:
|
||||
|
||||
```
|
||||
python3 scripts/ci/packages_release.py prepare [nodejs] [python]
|
||||
```
|
||||
|
||||
Both packages when none is listed.
|
||||
|
||||
Checked:
|
||||
- published library release recorded for the branch
|
||||
- add-on release published with every asset (Node.js)
|
||||
- types version published on npm (Node.js)
|
||||
- Python types unchanged since the commit of the pinned library release (Python)
|
||||
- pinned releases published with the pinned SHA-256
|
||||
- every pinned asset downloaded with a matching SHA-256
|
||||
|
||||
Written:
|
||||
- pins in `packages/simplex-chat-nodejs/src/release.ts` and `_version.py`: latest library release of the branch, add-on release of the current sources (Node.js), SHA-256 of every asset
|
||||
- `@simplex-chat/types` dependency: `^<types version>` (Node.js)
|
||||
- package versions: a published version bumped when the pins or the package files changed
|
||||
|
||||
`--nodejs-version` and `--python-version` set the versions, for the first release of an app version, e.g. `--nodejs-version 7.1.0-beta.7.1 --python-version 7.1.0b7`.
|
||||
|
||||
2. Commit, push to the same branch.
|
||||
|
||||
3. Run of `build.yml`:
|
||||
- build jobs, 5 platforms: packages tested, Node.js with the pinned add-on
|
||||
- `publish`: package tests of 5 platforms and pins checked, unpublished versions published with trusted publishing
|
||||
- `record`: published versions recorded
|
||||
|
||||
Versions unpublished after a failure: published by the next push to the branch.
|
||||
|
||||
## Local publication
|
||||
|
||||
1. Step 1 of the package release.
|
||||
|
||||
2. Package tests in a Python virtual environment, on the local platform (`linux-x86_64`, `linux-aarch64`, `macos-aarch64`, `macos-x86_64` or `windows-x86_64`):
|
||||
|
||||
```
|
||||
scripts/ci/package-tests.sh publish <platform> nodejs python
|
||||
```
|
||||
|
||||
3. In `packages/simplex-chat-nodejs`, with the npm tag of the version:
|
||||
|
||||
```
|
||||
npm publish --access public --tag beta
|
||||
```
|
||||
|
||||
4. In `packages/simplex-chat-python`:
|
||||
|
||||
```
|
||||
python -m pip install build twine
|
||||
python -m build
|
||||
twine upload dist/simplex_chat-<version>*
|
||||
```
|
||||
|
||||
5. Commit, push to the same branch. Locally published versions are skipped by `build.yml` and are not recorded in `releases.toml`.
|
||||
|
||||
## Settings
|
||||
|
||||
- simplex-chat-libs: release immutability enabled
|
||||
- secret `NODEJS_REPO_TOKEN`: contents of simplex-chat-libs, read and write
|
||||
- trusted publisher of `simplex-chat` and `@simplex-chat/types` on npm and of `simplex-chat` on PyPI: workflow `build.yml`
|
||||
- secrets `NPM_TOKEN` and `PYPI_API_TOKEN`: used instead of trusted publishing when set
|
||||
- variables `NPM_SCOPE`, `PYPI_PROJECT`, `PYPI_URL` and `PYPI_UPLOAD_URL`: publication under other names, for test repositories
|
||||
|
||||
## Pull requests
|
||||
|
||||
- `packages.yml`, for package changes:
|
||||
- core unchanged: add-on built and packages tested on 5 platforms
|
||||
- core changed: add-on built on 5 platforms when its sources changed
|
||||
- `build.yml`, for core changes: package tests on Linux x86_64 with the library built from the pull request
|
||||
@@ -4,6 +4,8 @@ This TypeScript library provides auto-generated types for bots API: commands and
|
||||
|
||||
It is used in [simplex-chat](https://www.npmjs.com/package/simplex-chat) Node.js library.
|
||||
|
||||
SimpleX Chat core: 7.1.0.12
|
||||
|
||||
[API reference](https://github.com/simplex-chat/simplex-chat/tree/stable/bots).
|
||||
|
||||
## License
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@simplex-chat/types",
|
||||
"version": "0.12.1",
|
||||
"version": "7.1.0-beta.6.1",
|
||||
"simplexChatCore": "7.1.0.12",
|
||||
"description": "TypeScript types for SimpleX Chat bot libraries",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "simplex-chat",
|
||||
"version": "7.1.0-beta.6.3",
|
||||
"version": "7.1.0-beta.6.4",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"gypfile": false,
|
||||
@@ -24,7 +24,7 @@
|
||||
"docs": "typedoc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@simplex-chat/types": "^0.12.1",
|
||||
"@simplex-chat/types": "^7.1.0-beta.6.1",
|
||||
"extract-zip": "^2.0.1",
|
||||
"fast-deep-equal": "^3.1.3"
|
||||
},
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// Written by scripts/ci/packages_release.py from the simplex-chat-libs releases.
|
||||
|
||||
export const LIBS_RELEASE = "core-7.1.0.11"
|
||||
export const LIBS_RELEASE = "core-7.1.0.12"
|
||||
|
||||
export const ADDON_RELEASE: string | undefined = "nodejs-addon-308c0960c707"
|
||||
|
||||
export const SHA256: {readonly [asset: string]: string} = {
|
||||
"simplex-chat-libs-linux-aarch64.zip": "d66aa1b30b9fa20225dad8ef75a9c605961e1c0adde2ba0283e2fcac4e15d260",
|
||||
"simplex-chat-libs-linux-x86_64-postgres.zip": "925b3166f969b12082ff8ee659e5f2a9933fe40120deb80c9aa3002a5ba6c803",
|
||||
"simplex-chat-libs-linux-x86_64.zip": "dabad0824e8813ea4a084fbf37e1ba4cf6dcea9cd8ccadc521fec200f246208a",
|
||||
"simplex-chat-libs-macos-aarch64.zip": "aa74608b149fff7508a47eaac4284dcc6d189c6ed8b7c18428df9158bc176a61",
|
||||
"simplex-chat-libs-macos-x86_64.zip": "a0e21457e1077be7f71b46c1c27452945b2cee2b6c50efb0004412ce5b7042ec",
|
||||
"simplex-chat-libs-windows-x86_64.zip": "c01dad574daa101a1cbfd31ddbf2b3d0d986a4a42f97d789d2f878b2050b2b4b",
|
||||
"simplex-chat-libs-linux-aarch64.zip": "924b8e14eaba5e309fa584c4bfe598421cf64983d63790c1c45247f592f60055",
|
||||
"simplex-chat-libs-linux-x86_64-postgres.zip": "323f908b45dde751d6082f8885d07d81fba8a5ab8b2df7d3b8ab3a87e56299e5",
|
||||
"simplex-chat-libs-linux-x86_64.zip": "995409e963db408bcf896361b669944484f845b6744fefe479aebb0ca2e7c5cd",
|
||||
"simplex-chat-libs-macos-aarch64.zip": "64464aa48b996e5a96b4aa651bc57bc69e412d3c3804b589c67bd9dc52faddbf",
|
||||
"simplex-chat-libs-macos-x86_64.zip": "94664f96ff7a2cd5a419096e7e843928faa31cb3addb8869fa1e4b5a0d0b3629",
|
||||
"simplex-chat-libs-windows-x86_64.zip": "2e10844e5c119921139c373ee2971341aceef7dc4198429d3be6b1570a009d15",
|
||||
"simplex-chat-nodejs-linux-aarch64.node": "3636a82c56c48229e0326fa03d6ceb5c16dd3afbcc1e86337783dc53a7328d67",
|
||||
"simplex-chat-nodejs-linux-x86_64.node": "55f3479b03fdb57591179ed730da744f1aa362e7529df55221b60028415746e9",
|
||||
"simplex-chat-nodejs-macos-aarch64.node": "a3472872fb0816f435f52ee5471088e6545ebffb95e89fbf8e1ab82275f2d56e",
|
||||
|
||||
@@ -56,15 +56,6 @@ ruff check && pyright && pytest tests/
|
||||
|
||||
Wire types under `src/simplex_chat/types/_*.py` are generated. Regenerate with `cabal test simplex-chat-test --test-options='--match Python'`.
|
||||
|
||||
## Release
|
||||
|
||||
Manual for now. Bump `_version.py:__version__`, build a wheel, upload to PyPI:
|
||||
|
||||
```bash
|
||||
uv build --wheel
|
||||
uv publish --token "$PYPI_TOKEN"
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[AGPL-3.0](./LICENSE)
|
||||
@@ -1,16 +1,16 @@
|
||||
"""Package version and the simplex-chat-libs release it loads.
|
||||
|
||||
Written by scripts/ci/packages_release.py: the release PR bumps the version
|
||||
Written by scripts/ci/packages_release.py prepare: it bumps the version
|
||||
and pins new library releases with the SHA-256 of their assets.
|
||||
"""
|
||||
|
||||
__version__ = "7.1.0b6" # PEP 440 — read by hatchling for wheel metadata
|
||||
LIBS_RELEASE = "core-7.1.0.11" # simplex-chat-libs release tag
|
||||
__version__ = "7.1.0b6.post1" # PEP 440 — read by hatchling for wheel metadata
|
||||
LIBS_RELEASE = "core-7.1.0.12" # simplex-chat-libs release tag
|
||||
LIBS_SHA256 = {
|
||||
"simplex-chat-libs-linux-aarch64.zip": "d66aa1b30b9fa20225dad8ef75a9c605961e1c0adde2ba0283e2fcac4e15d260",
|
||||
"simplex-chat-libs-linux-x86_64-postgres.zip": "925b3166f969b12082ff8ee659e5f2a9933fe40120deb80c9aa3002a5ba6c803",
|
||||
"simplex-chat-libs-linux-x86_64.zip": "dabad0824e8813ea4a084fbf37e1ba4cf6dcea9cd8ccadc521fec200f246208a",
|
||||
"simplex-chat-libs-macos-aarch64.zip": "aa74608b149fff7508a47eaac4284dcc6d189c6ed8b7c18428df9158bc176a61",
|
||||
"simplex-chat-libs-macos-x86_64.zip": "a0e21457e1077be7f71b46c1c27452945b2cee2b6c50efb0004412ce5b7042ec",
|
||||
"simplex-chat-libs-windows-x86_64.zip": "c01dad574daa101a1cbfd31ddbf2b3d0d986a4a42f97d789d2f878b2050b2b4b",
|
||||
"simplex-chat-libs-linux-aarch64.zip": "924b8e14eaba5e309fa584c4bfe598421cf64983d63790c1c45247f592f60055",
|
||||
"simplex-chat-libs-linux-x86_64-postgres.zip": "323f908b45dde751d6082f8885d07d81fba8a5ab8b2df7d3b8ab3a87e56299e5",
|
||||
"simplex-chat-libs-linux-x86_64.zip": "995409e963db408bcf896361b669944484f845b6744fefe479aebb0ca2e7c5cd",
|
||||
"simplex-chat-libs-macos-aarch64.zip": "64464aa48b996e5a96b4aa651bc57bc69e412d3c3804b589c67bd9dc52faddbf",
|
||||
"simplex-chat-libs-macos-x86_64.zip": "94664f96ff7a2cd5a419096e7e843928faa31cb3addb8869fa1e4b5a0d0b3629",
|
||||
"simplex-chat-libs-windows-x86_64.zip": "2e10844e5c119921139c373ee2971341aceef7dc4198429d3be6b1570a009d15",
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
# Package releases
|
||||
|
||||
Repository: simplex-chat/simplex-chat.
|
||||
Prerequisite: PR #7750 merged; master runs are not cancelled.
|
||||
Scope: releases in simplex-chat-libs, npm and PyPI.
|
||||
App releases (`v*` tags in simplex-chat): unchanged.
|
||||
|
||||
## Releases
|
||||
|
||||
| Release | Repository | Name | Released when | Assets |
|
||||
|---|---|---|---|---|
|
||||
| libsimplex | simplex-chat-libs | `core-<version of simplex-chat.cabal>` | push to master or stable claims the version for its branch | `LIBS_ASSETS` |
|
||||
| `@simplex-chat/types` | npm | version of its `package.json` | library release push, version absent from npm | |
|
||||
| add-on | simplex-chat-libs | `nodejs-addon-<7 characters of the pushed commit sha>` | push to master or stable, published release of the add-on hash absent | `ADDON_ASSETS` |
|
||||
| `simplex-chat` (npm) | npm | version of its `package.json` | push to master or stable, version absent from npm | |
|
||||
| `simplex-chat` (PyPI) | PyPI | `__version__` of `_version.py` | push to master or stable, version absent from PyPI | |
|
||||
|
||||
Release states in simplex-chat-libs:
|
||||
|
||||
```
|
||||
absent
|
||||
draft created before the build jobs; assets uploaded by the build jobs
|
||||
published every asset present; immutable
|
||||
```
|
||||
|
||||
Add-on hash: output of the current `addon_tag` function without its prefix.
|
||||
Add-on release notes: `sources: <add-on hash>`.
|
||||
|
||||
Types source: `packages/simplex-chat-client/types/typescript/src`.
|
||||
Python types source: `packages/simplex-chat-python/src/simplex_chat/types`.
|
||||
|
||||
## Library claim
|
||||
|
||||
Claim: entry `core.<branch>.<version>` = simplex-chat commit in `releases.toml` of simplex-chat-libs.
|
||||
Branches: master, stable.
|
||||
Written through the contents API with the blob sha; a concurrent writer re-reads after 409.
|
||||
|
||||
Rows in order of evaluation:
|
||||
|
||||
| `core.<branch>.<version>` | `claim-core` result |
|
||||
|---|---|
|
||||
| version lower than the highest version recorded for the branch | exit 1 |
|
||||
| version recorded on any branch for this commit | `resume` |
|
||||
| version recorded on any branch for another commit | `skip` |
|
||||
| absent, types check failed | exit 1 |
|
||||
| absent, types check passed | `new`: entry written |
|
||||
|
||||
Previous core release: commit of the highest version recorded for the branch.
|
||||
|
||||
Types check:
|
||||
|
||||
```
|
||||
types changed types source differs from the previous core release
|
||||
passed types unchanged
|
||||
or types version unrecorded, absent from npm, simplexChatCore = <version>
|
||||
```
|
||||
|
||||
## Types versions
|
||||
|
||||
```
|
||||
scheme <app version>.<n>, as simplex-chat (npm)
|
||||
first 7.1.0-beta.6.1, from core-release --types-version
|
||||
next bump_npm_version of the types version at the previous core release
|
||||
npm tag dist_tag: beta for a prerelease version, latest otherwise
|
||||
```
|
||||
|
||||
## Manual settings
|
||||
|
||||
1. simplex-chat-libs: immutable releases enabled.
|
||||
2. npm trusted publisher of `simplex-chat`: workflow `build.yml`.
|
||||
3. npm trusted publisher of `@simplex-chat/types`: workflow `build.yml`.
|
||||
4. PyPI trusted publisher of `simplex-chat`: workflow `build.yml`.
|
||||
5. Secret `RELEASE_PR_TOKEN`: deleted.
|
||||
6. PR #7732: closed; branch `packages-release/master`: deleted.
|
||||
7. `releases.toml` entry added: `core.master."7.1.0.11"` = `9711dcfc9c39ba125c1fb1674a3e039b1b9c0e81`.
|
||||
8. Notes of release `nodejs-addon-308c0960c707`: `sources: 308c0960c707`.
|
||||
|
||||
## Files
|
||||
|
||||
- `scripts/ci/packages_release.py`
|
||||
- `scripts/ci/test_packages_release.py`
|
||||
- `scripts/ci/package-tests.sh`: new.
|
||||
- `.github/workflows/build.yml`
|
||||
- `.github/workflows/packages.yml`
|
||||
- `packages/simplex-chat-client/types/typescript/package.json`: field `simplexChatCore`, `7.1.0.11`.
|
||||
- `packages/simplex-chat-client/types/typescript/README.md`: line `SimpleX Chat core: 7.1.0.11`.
|
||||
|
||||
## packages_release.py
|
||||
|
||||
Asset set by tag prefix:
|
||||
|
||||
```
|
||||
core- LIBS_ASSETS
|
||||
nodejs-addon- ADDON_ASSETS
|
||||
```
|
||||
|
||||
Commands:
|
||||
|
||||
```
|
||||
core-release <version> [--branch <branch>] [--types-version <version>]
|
||||
run locally
|
||||
branch: the checked out branch by default
|
||||
version not higher than the highest version recorded for the branch: exit 1
|
||||
version: of simplex-chat.cabal written
|
||||
types changed or --types-version: types version written, as in Types versions
|
||||
types version written: simplexChatCore and the README line written
|
||||
claim-core <version> <commit> <branch>
|
||||
run in maybe-release
|
||||
Library claim; prints claim=new, claim=resume or claim=skip
|
||||
new: also prints commit=<simplex-chat-libs commit of the entry>
|
||||
release-status <tag>
|
||||
prints status=published, status=draft or status=absent
|
||||
drafts: from the authenticated release list
|
||||
addon-release
|
||||
releases nodejs-addon-* with notes "sources: <add-on hash of the checkout>"
|
||||
published one: prints tag=<tag> and status=published
|
||||
draft: prints tag=<tag> and status=draft
|
||||
neither: prints status=absent
|
||||
latest-libs [--branch <branch>]
|
||||
prints tag=<newest published core-* release of a version recorded for the branch, with every LIBS_ASSETS digest>
|
||||
order: numeric components of the version
|
||||
publish-release <tag>
|
||||
draft with every asset of its set: published
|
||||
missing assets: listed, exit 1
|
||||
check-pins
|
||||
pins of release.ts and _version.py compared with the digests of published releases
|
||||
mismatch, missing asset or unpublished release: listed, exit 1
|
||||
download <tag> <asset> <dir>
|
||||
asset saved to <dir>/<asset>
|
||||
SHA-256 compared with the API digest; mismatch: exit 1
|
||||
download-pinned <asset> <dir>
|
||||
tag and SHA-256 from release.ts
|
||||
asset saved to <dir>/<asset>; mismatch: exit 1
|
||||
prepare [nodejs] [python] [--branch <branch>]
|
||||
run locally
|
||||
branch: the checked out branch by default
|
||||
nodejs: simplex-chat (npm); python: simplex-chat (PyPI)
|
||||
packages: the listed ones; all when none is listed
|
||||
other package: pins and version unchanged
|
||||
library pin: latest-libs of the branch
|
||||
warning: core-<version of simplex-chat.cabal> newer than the pinned release
|
||||
nodejs add-on pin: addon-release; exit 1 unless published with every asset
|
||||
nodejs types dependency: ^<types version of the branch>; exit 1 unless published on npm
|
||||
python: exit 1 when the Python types source differs from the commit recorded for the pinned core version
|
||||
every pinned asset: download, SHA-256 compared with the API digest
|
||||
changed package: directory without tests differs from the commit that set its version
|
||||
versions: bumped as now; --nodejs-version, --python-version override
|
||||
written: release.ts, _version.py, package.json of simplex-chat (npm)
|
||||
summary: printed; --summary and --types-version removed
|
||||
build-plan
|
||||
pull requests only
|
||||
--event and --release-missing removed
|
||||
```
|
||||
|
||||
Commands removed: `addon-tag`.
|
||||
`record`: option `--branch` of `--core`, default master; replaced by `claim-core` in maybe-release.
|
||||
Unchanged: `changes`, `publish-plan`, `test-names`.
|
||||
|
||||
`build_plan(core_changed, addon_changed)`:
|
||||
|
||||
```
|
||||
core changed, add-on changed platforms: all; test: false
|
||||
core changed, add-on unchanged platforms: none; test: false
|
||||
core unchanged platforms: all; test: true
|
||||
```
|
||||
|
||||
## package-tests.sh
|
||||
|
||||
Commands from the build steps of `packages.yml` (lines 147-166).
|
||||
|
||||
```
|
||||
build <platform>
|
||||
types, npm package and add-on built
|
||||
add-on copied to packages/simplex-chat-nodejs/build/simplex-chat-nodejs-<platform>.node
|
||||
test
|
||||
jest, as in packages.yml; SIMPLEX_LIBS_DIR and SIMPLEX_ADDON_PATH from the environment
|
||||
pytest
|
||||
addon <platform>
|
||||
build <platform>
|
||||
simplex-chat-libs-<platform>.zip of $LIBS_RELEASE: packages_release.py download, unpack to $RUNNER_TEMP/libs
|
||||
jest with SIMPLEX_LIBS_DIR=$RUNNER_TEMP/libs/libs
|
||||
add-on uploaded to draft $ADDON_TAG of $LIBS_REPOSITORY: gh release upload --clobber, token $GH_TOKEN
|
||||
publish <platform> [nodejs] [python]
|
||||
nodejs: types and npm package built
|
||||
download-pinned simplex-chat-nodejs-<platform>.node to $RUNNER_TEMP/addon
|
||||
add-on copied to packages/simplex-chat-nodejs/build/Release/simplex.node, the path of the tests
|
||||
jest
|
||||
python: pytest
|
||||
run in CI and locally
|
||||
```
|
||||
|
||||
## build.yml
|
||||
|
||||
### maybe-release
|
||||
|
||||
Outputs:
|
||||
|
||||
```
|
||||
libs_tag core-<version>, when the claim is new or resume
|
||||
addon_tag tag of the add-on draft
|
||||
addon_draft true when the add-on release is a draft after this job
|
||||
libs_latest output tag of latest-libs
|
||||
publish_types libs_tag set, types version printed by publish-plan
|
||||
publish_nodejs nodejs version printed by publish-plan
|
||||
publish_python python version printed by publish-plan
|
||||
publish publish_nodejs or publish_python
|
||||
```
|
||||
|
||||
Steps after "Create release":
|
||||
|
||||
| Step | Condition | Action |
|
||||
|---|---|---|
|
||||
| Library release | push to master or stable | `claim-core <version> <github.sha> <github.ref_name>`; new → `gh release create core-<version> --draft --target <commit>`; resume → existing draft, `libs_tag` empty when published; skip → notice annotation |
|
||||
| Add-on release | push to master or stable | `addon-release`: absent → `gh release create nodejs-addon-<7 characters of github.sha> --draft --notes "sources: <add-on hash>"`; draft → reused; output `addon_draft` true when absent or draft |
|
||||
| Latest library | `addon_draft` | `latest-libs --branch <github.ref_name>` |
|
||||
| Versions to publish | push to master or stable | `publish-plan`; environment `NPM_SCOPE`, `PYPI_PROJECT`, `PYPI_URL` as in `packages.yml` |
|
||||
|
||||
Steps removed: "Check version change", "Record libs release", "Create libs release".
|
||||
"Clone project": `fetch-depth: 2`, unchanged; the types check fetches the commit of the recorded types version.
|
||||
Token of the release steps: `NODEJS_REPO_TOKEN`.
|
||||
Library release jobs: concurrency groups with `libs_tag`, unchanged.
|
||||
|
||||
### Library uploads
|
||||
|
||||
Steps "Upload library" (build-linux, build-macos, build-windows) and "Upload postgres library":
|
||||
|
||||
```
|
||||
action gh release upload <libs_tag> <zip files> --clobber --repo <repository>-libs
|
||||
token NODEJS_REPO_TOKEN
|
||||
conditions unchanged
|
||||
```
|
||||
|
||||
### Package steps in build jobs
|
||||
|
||||
Platform jobs:
|
||||
|
||||
```
|
||||
build-linux matrix.libs entry linux-x86_64
|
||||
build-linux-push matrix.libs entry linux-aarch64
|
||||
build-macos both entries macos-<arch>
|
||||
build-windows windows-latest windows-x86_64
|
||||
```
|
||||
|
||||
Steps, after "Run tests" (build-linux, build-macos) and after "Upload library" (build-windows):
|
||||
|
||||
| Step | Condition |
|
||||
|---|---|
|
||||
| Setup Node.js 22 | `!cancelled()`; core pull request condition, `addon_draft` or `publish` |
|
||||
| Setup Python 3.12 | `!cancelled()`, Setup Node.js succeeded |
|
||||
| Package tests (pull request) | build-linux only, as now; `package-tests.sh build linux-x86_64`, then `test` with SIMPLEX_LIBS_DIR |
|
||||
| Add-on | `!cancelled()`, `addon_draft`, Setup Python succeeded; `package-tests.sh addon <platform>` |
|
||||
| Package tests | `!cancelled()`, `publish`, Setup Python succeeded; `package-tests.sh publish <platform>`, with `nodejs` when `publish_nodejs`, with `python` when `publish_python` |
|
||||
| Package tests result | Package tests succeeded; artifact `package-tests-<platform>` |
|
||||
|
||||
Linux steps: on the host.
|
||||
|
||||
Environment of "Add-on":
|
||||
|
||||
```
|
||||
LIBS_RELEASE libs_latest
|
||||
ADDON_TAG addon_tag
|
||||
LIBS_REPOSITORY <repository>-libs
|
||||
GH_TOKEN NODEJS_REPO_TOKEN
|
||||
```
|
||||
|
||||
### Final jobs
|
||||
|
||||
```
|
||||
releases
|
||||
needs maybe-release, build-linux, build-linux-push, build-linux-postgres, build-macos, build-windows
|
||||
if !cancelled(), libs_tag or addon_draft
|
||||
step 1 publish-release <libs_tag>; libs_tag
|
||||
step 2 publish-release <addon_tag>; !cancelled(), addon_draft
|
||||
token NODEJS_REPO_TOKEN
|
||||
publish
|
||||
needs maybe-release, build-linux, build-linux-push, build-macos, build-windows, releases
|
||||
if !cancelled(), publish or publish_types
|
||||
permissions contents: read; id-token: write
|
||||
env as in packages.yml: NODE_AUTH_TOKEN, NPM_SCOPE, PYPI_PROJECT, PYPI_URL
|
||||
step 1 when publish: artifacts package-tests-*, 5 required
|
||||
step 2 when publish: check-pins
|
||||
steps "publish" job of packages.yml, from "Versions to publish"
|
||||
types step publish_types and needs.releases.result == 'success'
|
||||
record
|
||||
as in packages.yml; needs publish
|
||||
```
|
||||
|
||||
Comments of the `publish` job of `packages.yml`: moved with it.
|
||||
Comment of the `record` job: removed.
|
||||
Trusted publishing comment (`packages.yml` lines 12-15): moved above `publish`.
|
||||
|
||||
Job removed: `packages`, with its header comment.
|
||||
|
||||
## packages.yml
|
||||
|
||||
```
|
||||
triggers pull_request, paths unchanged
|
||||
push, workflow_dispatch, workflow_call: removed
|
||||
addon job steps "Add-on release tag", "Release branch", "Check the add-on release": removed
|
||||
outputs tag, release_branch, missing: removed
|
||||
Build plan: --event and --release-missing removed
|
||||
build job Checkout, Setup Node.js, Setup Python
|
||||
package-tests.sh build <platform>
|
||||
package-tests.sh test, when planned
|
||||
"Upload add-on artifact": removed
|
||||
jobs removed publish, record, release
|
||||
header lines 3-10 and 17-28: removed
|
||||
```
|
||||
|
||||
## Release procedure
|
||||
|
||||
Library and types:
|
||||
|
||||
1. `python3 scripts/ci/packages_release.py core-release <version>` on master or stable, with `--types-version` for a new app version.
|
||||
2. Commit of the written files; push to the same branch.
|
||||
3. `build.yml`: claim, draft, uploads by the build jobs, `releases`, types publication.
|
||||
4. Failed claim: fix commit; the next push claims the version.
|
||||
5. Incomplete draft: re-run of the failed build jobs, then of `releases` and `publish`.
|
||||
|
||||
Add-on:
|
||||
|
||||
1. Push to master or stable changes the add-on sources.
|
||||
2. Incomplete draft: completed by a re-run or by the next push.
|
||||
|
||||
Packages, published by GitHub:
|
||||
|
||||
1. `git pull` on master or stable.
|
||||
2. `python3 scripts/ci/packages_release.py prepare`, with package names and version overrides when needed.
|
||||
3. Commit of the written files; push to the same branch.
|
||||
4. Versions absent from the registries: published by `build.yml`.
|
||||
|
||||
Packages, published locally:
|
||||
|
||||
1. Steps 1 and 2 above.
|
||||
2. `scripts/ci/package-tests.sh publish <local platform> [nodejs] [python]`.
|
||||
3. `npm publish` of `simplex-chat`; `python -m build` and `twine upload` of the Python package.
|
||||
4. Step 3 above; `build.yml` skips the published versions.
|
||||
|
||||
Published npm package: `files` of `package.json` and the pins; add-on and libsimplex downloaded at run time.
|
||||
|
||||
## Verification
|
||||
|
||||
1. Unit tests:
|
||||
- `claim-core`: every row of the Library claim table
|
||||
- types check: changed and unchanged types, recorded and unrecorded types version
|
||||
- `core-release` with and without types changes
|
||||
- `release-status`
|
||||
- `addon-release` notes lookup
|
||||
- `prepare` with one package listed
|
||||
- `prepare python` with changed Python types
|
||||
- `latest-libs` order
|
||||
- `publish-release` with missing assets
|
||||
- `check-pins` mismatch
|
||||
- `download` digest mismatch
|
||||
- `build_plan` cases
|
||||
2. `gh release upload` to a draft by tag: `shared.FetchRelease` of gh looks up the draft by tag (`fetchDraftRelease`).
|
||||
3. First library release after the merge: assets uploaded to the draft, release published by `publish-release`.
|
||||
4. First master push after merge: claim `skip`, as `core-7.1.0.11` is recorded for `9711dcfc`; `addon-release` finds `nodejs-addon-308c0960c707`; package steps skipped.
|
||||
5. Local `prepare`: pins `core-7.1.0.11` and `nodejs-addon-308c0960c707`; every download matches.
|
||||
6. `core-release 7.1.0.12 --types-version 7.1.0-beta.6.1`, commit, push: claim `new`, draft, 6 assets uploaded, release published, types 7.1.0-beta.6.1 published with tag beta.
|
||||
7. Master push with an add-on source change: draft created, 5 assets uploaded, release published.
|
||||
8. Master push with versions from `prepare`: 5 artifacts `package-tests-*`, pins checked, versions published, versions recorded.
|
||||
9. Second push during the run of step 6: claim `skip`.
|
||||
10. Stable push after master is merged into stable, with a stable core version: claim `new` for `core.stable`.
|
||||
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
nodejs=packages/simplex-chat-nodejs
|
||||
python_package=packages/simplex-chat-python
|
||||
python=$(command -v python3 || command -v python)
|
||||
temp=${RUNNER_TEMP:-$(mktemp -d)}
|
||||
|
||||
usage() {
|
||||
echo "usage: $0 build <platform> | test | addon <platform> | publish <platform> [nodejs] [python]" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
build_package() {
|
||||
(cd packages/simplex-chat-client/types/typescript && npm install --no-audit --no-fund && npx tsc)
|
||||
(cd "$nodejs" && npm install --ignore-scripts --no-save --no-audit --no-fund ../simplex-chat-client/types/typescript)
|
||||
if [ -n "${1:-}" ]; then
|
||||
(cd "$nodejs" && npx --yes node-gyp rebuild)
|
||||
fi
|
||||
(cd "$nodejs" && npx tsc && cp src/simplex.* dist)
|
||||
if [ -n "${1:-}" ]; then
|
||||
cp "$nodejs/build/Release/simplex.node" "$nodejs/build/simplex-chat-nodejs-$1.node"
|
||||
fi
|
||||
}
|
||||
|
||||
test_nodejs() {
|
||||
(cd "$nodejs" && npx jest --forceExit --testNamePattern '^(?!.*use preset servers)')
|
||||
}
|
||||
|
||||
test_python() {
|
||||
(unset SIMPLEX_LIBS_DIR && cd "$python_package" && "$python" -m pip install -e ".[test]" && "$python" -m pytest -q)
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
build)
|
||||
[ $# -eq 2 ] || usage
|
||||
build_package "$2"
|
||||
;;
|
||||
test)
|
||||
[ $# -eq 1 ] || usage
|
||||
test_nodejs
|
||||
test_python
|
||||
;;
|
||||
addon)
|
||||
[ $# -eq 2 ] || usage
|
||||
build_package "$2"
|
||||
zip=simplex-chat-libs-$2.zip
|
||||
"$python" scripts/ci/packages_release.py download "$LIBS_RELEASE" "$zip" "$temp/libs"
|
||||
"$python" -m zipfile -e "$temp/libs/$zip" "$temp/libs"
|
||||
SIMPLEX_LIBS_DIR=$temp/libs/libs test_nodejs
|
||||
gh release upload "$ADDON_TAG" "$nodejs/build/simplex-chat-nodejs-$2.node" --clobber --repo "$LIBS_REPOSITORY"
|
||||
;;
|
||||
publish)
|
||||
[ $# -ge 2 ] || usage
|
||||
platform=$2
|
||||
shift 2
|
||||
for suite in "$@"; do
|
||||
case "$suite" in
|
||||
nodejs)
|
||||
build_package
|
||||
addon=simplex-chat-nodejs-$platform.node
|
||||
"$python" scripts/ci/packages_release.py download-pinned "$addon" "$temp/addon"
|
||||
mkdir -p "$nodejs/build/Release"
|
||||
cp "$temp/addon/$addon" "$nodejs/build/Release/simplex.node"
|
||||
test_nodejs
|
||||
;;
|
||||
python)
|
||||
test_python
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
+442
-106
@@ -1,13 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Release automation for simplex-chat and @simplex-chat/types on npm and simplex-chat on PyPI.
|
||||
|
||||
addon-tag print the add-on release tag for the current sources
|
||||
changes print whether the changes since a commit touch the chat core and the add-on
|
||||
build-plan print the platforms to build the add-on on and whether to run the package tests
|
||||
prepare update pins and versions for a release PR
|
||||
publish-plan print the package versions that are not published yet
|
||||
record record releases and published versions in releases.toml of simplex-chat-libs
|
||||
test-names rename the packages to NPM_SCOPE and PYPI_PROJECT (test repositories)
|
||||
core-release set the core version and the types version for a library release
|
||||
claim-core claim the library release of the core version for a commit
|
||||
release-status print whether a release of simplex-chat-libs is published, a draft or absent
|
||||
addon-release print the add-on release of the current add-on sources
|
||||
latest-libs print the newest published library release
|
||||
publish-release publish a draft release that has every asset
|
||||
check-pins check the pins of the packages against the published releases
|
||||
download download a release asset and check its digest
|
||||
download-pinned download a pinned asset and check its pin
|
||||
changes print whether the changes since a commit touch the chat core and the add-on
|
||||
build-plan print the platforms to build the add-on on and whether to run the package tests
|
||||
prepare update pins and versions of the packages
|
||||
publish-plan print the package versions that are not published yet
|
||||
record record releases and published versions in releases.toml of simplex-chat-libs
|
||||
test-names rename the packages to NPM_SCOPE and PYPI_PROJECT (test repositories)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -19,12 +27,14 @@ import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import tomllib
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Collection
|
||||
from dataclasses import dataclass, field, replace
|
||||
from typing import Any
|
||||
from pathlib import Path
|
||||
@@ -37,7 +47,15 @@ RELEASE_FILE = f"{NODEJS_DIR}/src/release.ts"
|
||||
PYTHON_VERSION_FILE = f"{PYTHON_DIR}/src/simplex_chat/_version.py"
|
||||
ADDON_SOURCES = [f"{NODEJS_DIR}/binding.gyp", f"{NODEJS_DIR}/cpp"]
|
||||
CORE_SOURCES = ["src", "simplex-chat.cabal", "cabal.project"]
|
||||
ADDON_TAG_HASH_LENGTH = 12
|
||||
CABAL_FILE = "simplex-chat.cabal"
|
||||
CABAL_VERSION = re.compile(r"^(version:\s*)(\S+)", re.M)
|
||||
TYPES_SOURCES = [f"{TYPES_DIR}/src"]
|
||||
TYPES_README = f"{TYPES_DIR}/README.md"
|
||||
TYPES_README_CORE = re.compile(r"^SimpleX Chat core: .*$", re.M)
|
||||
PYTHON_TYPES_SOURCES = [f"{PYTHON_DIR}/src/simplex_chat/types"]
|
||||
ADDON_HASH_LENGTH = 12
|
||||
CORE_TAG = re.compile(r"^core-(\d+(?:\.\d+)*)$")
|
||||
ADDON_TAG_PREFIX = "nodejs-addon-"
|
||||
LIBS_ASSETS = [
|
||||
"simplex-chat-libs-linux-aarch64.zip",
|
||||
"simplex-chat-libs-linux-x86_64-postgres.zip",
|
||||
@@ -58,18 +76,27 @@ ADDON_ASSETS = [f"simplex-chat-nodejs-{platform}.node" for platform in ADDON_PLA
|
||||
SHA256_HEX = re.compile(r"^[0-9a-f]{64}$")
|
||||
RELEASES_FILE = "releases.toml"
|
||||
RECORD_ATTEMPTS = 5
|
||||
DOWNLOAD_CHUNK = 1 << 20
|
||||
TOML_BARE_KEY = re.compile(r"^[A-Za-z0-9_-]+$")
|
||||
|
||||
|
||||
# Pure functions
|
||||
|
||||
|
||||
def addon_tag(files: dict[str, bytes], node_addon_api: str) -> str:
|
||||
def addon_hash(files: dict[str, bytes], node_addon_api: str) -> str:
|
||||
digest = hashlib.sha256()
|
||||
for name in sorted(files):
|
||||
digest.update(name.encode() + b"\0" + files[name] + b"\0")
|
||||
digest.update(f"node-addon-api@{node_addon_api}".encode())
|
||||
return f"nodejs-addon-{digest.hexdigest()[:ADDON_TAG_HASH_LENGTH]}"
|
||||
return digest.hexdigest()[:ADDON_HASH_LENGTH]
|
||||
|
||||
|
||||
def addon_notes(sources_hash: str) -> str:
|
||||
return f"sources: {sources_hash}"
|
||||
|
||||
|
||||
def version_key(version: str) -> tuple[int, ...]:
|
||||
return tuple(int(part) for part in version.split("."))
|
||||
|
||||
|
||||
def bump_npm_version(version: str) -> str:
|
||||
@@ -141,7 +168,7 @@ def render_python_version(version: str, pins: Pins) -> str:
|
||||
return (
|
||||
'"""Package version and the simplex-chat-libs release it loads.\n'
|
||||
"\n"
|
||||
"Written by scripts/ci/packages_release.py: the release PR bumps the version\n"
|
||||
"Written by scripts/ci/packages_release.py prepare: it bumps the version\n"
|
||||
"and pins new library releases with the SHA-256 of their assets.\n"
|
||||
'"""\n'
|
||||
"\n"
|
||||
@@ -152,16 +179,84 @@ def render_python_version(version: str, pins: Pins) -> str:
|
||||
)
|
||||
|
||||
|
||||
def asset_digests(release: dict) -> dict[str, str]:
|
||||
return {a["name"]: (a.get("digest") or "").removeprefix("sha256:") for a in release["assets"]}
|
||||
|
||||
|
||||
def release_digests(release: dict | None, assets: list[str]) -> dict[str, str] | None:
|
||||
"""Asset digests of a GitHub release, or None when the release is missing or incomplete."""
|
||||
if release is None:
|
||||
return None
|
||||
digests = {a["name"]: (a.get("digest") or "").removeprefix("sha256:") for a in release["assets"]}
|
||||
digests = asset_digests(release)
|
||||
if not all(SHA256_HEX.match(digests.get(asset, "")) for asset in assets):
|
||||
return None
|
||||
return {asset: digests[asset] for asset in assets}
|
||||
|
||||
|
||||
def release_assets(tag: str) -> list[str]:
|
||||
if CORE_TAG.match(tag):
|
||||
return LIBS_ASSETS
|
||||
if tag.startswith(ADDON_TAG_PREFIX):
|
||||
return ADDON_ASSETS
|
||||
raise ValueError(f"no asset set for release {tag}")
|
||||
|
||||
|
||||
def preferred_release(releases: list[dict]) -> dict | None:
|
||||
"""The published release among them, otherwise the first draft."""
|
||||
return next((r for r in releases if not r["draft"]), next(iter(releases), None))
|
||||
|
||||
|
||||
def release_status(release: dict | None) -> str:
|
||||
if release is None:
|
||||
return "absent"
|
||||
return "draft" if release["draft"] else "published"
|
||||
|
||||
|
||||
def tag_release(releases: list[dict], tag: str) -> dict | None:
|
||||
return preferred_release([r for r in releases if r["tag_name"] == tag])
|
||||
|
||||
|
||||
def addon_release(releases: list[dict], sources_hash: str) -> dict | None:
|
||||
notes = addon_notes(sources_hash)
|
||||
return preferred_release(
|
||||
[r for r in releases if r["tag_name"].startswith(ADDON_TAG_PREFIX) and r.get("body") == notes]
|
||||
)
|
||||
|
||||
|
||||
def core_version(tag: str) -> str | None:
|
||||
match = CORE_TAG.match(tag)
|
||||
return match[1] if match else None
|
||||
|
||||
|
||||
def latest_libs(releases: list[dict], versions: Collection[str]) -> str | None:
|
||||
"""The newest published release of the core versions that has every library asset."""
|
||||
tags = [
|
||||
r["tag_name"]
|
||||
for r in releases
|
||||
if not r["draft"] and core_version(r["tag_name"]) in versions and release_digests(r, LIBS_ASSETS)
|
||||
]
|
||||
return max(tags, key=lambda tag: version_key(core_version(tag)), default=None)
|
||||
|
||||
|
||||
def missing_assets(release: dict) -> list[str]:
|
||||
present = {a["name"] for a in release["assets"]}
|
||||
return [asset for asset in release_assets(release["tag_name"]) if asset not in present]
|
||||
|
||||
|
||||
def pin_problems(pins: Pins, published: dict[str, dict | None]) -> list[str]:
|
||||
"""Differences between the pins and the published releases of the pinned tags."""
|
||||
problems = []
|
||||
for tag, release in published.items():
|
||||
if release is None:
|
||||
problems.append(f"{tag} is not published")
|
||||
continue
|
||||
digests = asset_digests(release)
|
||||
for asset in release_assets(tag):
|
||||
if pins.sha256.get(asset) != digests.get(asset):
|
||||
problems.append(f"{asset} of {tag}: pinned {pins.sha256.get(asset)}, published {digests.get(asset)}")
|
||||
return problems
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Package:
|
||||
version: str
|
||||
@@ -177,10 +272,11 @@ class State:
|
||||
addon_digests: dict[str, str] | None
|
||||
nodejs_pins: Pins
|
||||
python_pins: Pins
|
||||
types: Package
|
||||
types_dependency: str
|
||||
nodejs_types_dependency: str
|
||||
nodejs: Package
|
||||
python: Package
|
||||
types_version: str | None = None
|
||||
packages: frozenset[str] = frozenset({"nodejs", "python"})
|
||||
nodejs_version: str | None = None
|
||||
python_version: str | None = None
|
||||
|
||||
@@ -189,23 +285,16 @@ class State:
|
||||
class Plan:
|
||||
nodejs_pins: Pins
|
||||
python_pins: Pins
|
||||
types_version: str
|
||||
types_bumped: bool
|
||||
types_dependency: str | None
|
||||
nodejs_version: str
|
||||
nodejs_changed: bool
|
||||
python_version: str
|
||||
python_changed: bool
|
||||
notes: list[str] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def types_dependency(self) -> str | None:
|
||||
return f"^{self.types_version}" if self.types_bumped else None
|
||||
|
||||
@property
|
||||
def title(self) -> str:
|
||||
parts = []
|
||||
if self.types_bumped:
|
||||
parts.append(f"types {self.types_version}")
|
||||
if self.nodejs_changed:
|
||||
parts.append(f"nodejs {self.nodejs_version}")
|
||||
if self.python_changed:
|
||||
@@ -214,7 +303,7 @@ class Plan:
|
||||
|
||||
|
||||
def plan(state: State) -> Plan:
|
||||
"""What a release PR changes, from the state of the branch and the releases."""
|
||||
"""What a package release changes, from the state of the branch and the releases."""
|
||||
notes: list[str] = []
|
||||
|
||||
def pin(pins: Pins, field_name: str, tag: str, digests: dict[str, str] | None, what: str) -> tuple[Pins, bool]:
|
||||
@@ -227,27 +316,30 @@ def plan(state: State) -> Plan:
|
||||
notes.append(f"{what}: {tag}")
|
||||
return replace(pins, **{field_name: tag, "sha256": {**pins.sha256, **digests}}), True
|
||||
|
||||
nodejs_pins, nodejs_libs = pin(
|
||||
state.nodejs_pins, "libs_release", state.libs_tag, state.libs_digests, "Node.js libsimplex"
|
||||
)
|
||||
nodejs_pins, addon = pin(nodejs_pins, "addon_release", state.addon_tag, state.addon_digests, "Node.js add-on")
|
||||
python_pins, python_libs = pin(
|
||||
state.python_pins, "libs_release", state.libs_tag, state.libs_digests, "Python libsimplex"
|
||||
)
|
||||
|
||||
def next_version(pkg: Package, changed: bool, override: str | None, bump) -> str:
|
||||
if override:
|
||||
return override
|
||||
return bump(pkg.version) if changed and pkg.published else pkg.version
|
||||
|
||||
types_version = next_version(state.types, state.types.changed, state.types_version, bump_npm_version)
|
||||
types_bumped = types_version != state.types.version
|
||||
nodejs_changed = nodejs_libs or addon or types_bumped or state.nodejs.changed
|
||||
nodejs_version = next_version(state.nodejs, nodejs_changed, state.nodejs_version, bump_npm_version)
|
||||
python_changed = python_libs or state.python.changed
|
||||
python_version = next_version(state.python, python_changed, state.python_version, bump_python_version)
|
||||
nodejs_pins, nodejs_version, nodejs_changed, types_dependency = state.nodejs_pins, state.nodejs.version, False, None
|
||||
if "nodejs" in state.packages:
|
||||
nodejs_pins, nodejs_libs = pin(
|
||||
state.nodejs_pins, "libs_release", state.libs_tag, state.libs_digests, "Node.js libsimplex"
|
||||
)
|
||||
nodejs_pins, addon = pin(nodejs_pins, "addon_release", state.addon_tag, state.addon_digests, "Node.js add-on")
|
||||
if state.types_dependency != state.nodejs_types_dependency:
|
||||
types_dependency = state.types_dependency
|
||||
notes.append(f"@simplex-chat/types: {types_dependency}")
|
||||
nodejs_changed = nodejs_libs or addon or types_dependency is not None or state.nodejs.changed
|
||||
nodejs_version = next_version(state.nodejs, nodejs_changed, state.nodejs_version, bump_npm_version)
|
||||
python_pins, python_version, python_changed = state.python_pins, state.python.version, False
|
||||
if "python" in state.packages:
|
||||
python_pins, python_libs = pin(
|
||||
state.python_pins, "libs_release", state.libs_tag, state.libs_digests, "Python libsimplex"
|
||||
)
|
||||
python_changed = python_libs or state.python.changed
|
||||
python_version = next_version(state.python, python_changed, state.python_version, bump_python_version)
|
||||
for name, old, new in [
|
||||
("@simplex-chat/types", state.types.version, types_version),
|
||||
("simplex-chat (npm)", state.nodejs.version, nodejs_version),
|
||||
("simplex-chat (PyPI)", state.python.version, python_version),
|
||||
]:
|
||||
@@ -256,8 +348,7 @@ def plan(state: State) -> Plan:
|
||||
return Plan(
|
||||
nodejs_pins=nodejs_pins,
|
||||
python_pins=python_pins,
|
||||
types_version=types_version,
|
||||
types_bumped=types_bumped,
|
||||
types_dependency=types_dependency,
|
||||
nodejs_version=nodejs_version,
|
||||
nodejs_changed=nodejs_changed or nodejs_version != state.nodejs.version,
|
||||
python_version=python_version,
|
||||
@@ -272,31 +363,49 @@ class BuildPlan:
|
||||
test: bool
|
||||
|
||||
|
||||
def build_plan(
|
||||
event: str, release_missing: bool, libs_release: str, core_changed: bool, addon_changed: bool
|
||||
) -> BuildPlan:
|
||||
"""Each package test runs once, against the libsimplex of the change: a pull request that changes the core
|
||||
is tested by build.yml with its own libsimplex, a new library release with that release."""
|
||||
if libs_release:
|
||||
return BuildPlan(ADDON_PLATFORMS, test=True)
|
||||
if event == "pull_request":
|
||||
if core_changed:
|
||||
return BuildPlan(ADDON_PLATFORMS if addon_changed else [], test=False)
|
||||
return BuildPlan(ADDON_PLATFORMS, test=True)
|
||||
return BuildPlan(ADDON_PLATFORMS if release_missing else [], test=True)
|
||||
def build_plan(core_changed: bool, addon_changed: bool) -> BuildPlan:
|
||||
"""The package tests of a pull request that changes the core run in build.yml, with its own libsimplex."""
|
||||
if core_changed:
|
||||
return BuildPlan(ADDON_PLATFORMS if addon_changed else [], test=False)
|
||||
return BuildPlan(ADDON_PLATFORMS, test=True)
|
||||
|
||||
|
||||
def build_matrix(platforms: list[str]) -> str:
|
||||
return json.dumps([{"platform": platform, "os": ADDON_RUNNERS[platform]} for platform in platforms])
|
||||
|
||||
|
||||
def recorded_commit(releases: dict, key: str, version: str) -> str | None:
|
||||
"""The simplex-chat commit recorded for a version; key is "core", "npm/<name>" or "pypi/<name>"."""
|
||||
def core_key(branch: str) -> str:
|
||||
return f"core/{branch}"
|
||||
|
||||
|
||||
def recorded_versions(releases: dict, key: str) -> dict[str, str]:
|
||||
"""The recorded versions and their simplex-chat commits; key is "core/<branch>", "npm/<name>" or "pypi/<name>"."""
|
||||
table = releases
|
||||
for part in key.split("/", 1):
|
||||
table = table.get(part, {})
|
||||
commit = table.get(version)
|
||||
return commit if isinstance(commit, str) else None
|
||||
return {version: commit for version, commit in table.items() if isinstance(commit, str)}
|
||||
|
||||
|
||||
def recorded_commit(releases: dict, key: str, version: str) -> str | None:
|
||||
return recorded_versions(releases, key).get(version)
|
||||
|
||||
|
||||
def highest_recorded(releases: dict, key: str) -> str | None:
|
||||
return max(recorded_versions(releases, key), key=version_key, default=None)
|
||||
|
||||
|
||||
def recorded_core_commit(releases: dict, version: str) -> str | None:
|
||||
"""The commit recorded for the core version on any branch: a release tag is shared by the branches."""
|
||||
for branch in releases.get("core", {}):
|
||||
if commit := recorded_commit(releases, core_key(branch), version):
|
||||
return commit
|
||||
return None
|
||||
|
||||
|
||||
def previous_core_commit(releases: dict, branch: str) -> str | None:
|
||||
"""The commit of the highest core version recorded for the branch."""
|
||||
previous = highest_recorded(releases, core_key(branch))
|
||||
return recorded_commit(releases, core_key(branch), previous) if previous else None
|
||||
|
||||
|
||||
def add_releases(releases: dict, entries: list[tuple[str, str]], commit: str) -> dict:
|
||||
@@ -357,14 +466,18 @@ def write_json(file: str, value: dict) -> None:
|
||||
(ROOT / file).write_text(json.dumps(value, indent=2, ensure_ascii=False) + "\n")
|
||||
|
||||
|
||||
def current_addon_tag() -> str:
|
||||
def current_addon_hash() -> str:
|
||||
files = {
|
||||
path.relative_to(ROOT).as_posix(): path.read_bytes()
|
||||
for source in ADDON_SOURCES
|
||||
for path in ([ROOT / source] if (ROOT / source).is_file() else sorted((ROOT / source).rglob("*")))
|
||||
if path.is_file()
|
||||
}
|
||||
return addon_tag(files, read_json(f"{NODEJS_DIR}/package.json")["devDependencies"]["node-addon-api"])
|
||||
return addon_hash(files, read_json(f"{NODEJS_DIR}/package.json")["devDependencies"]["node-addon-api"])
|
||||
|
||||
|
||||
def cabal_version() -> str:
|
||||
return CABAL_VERSION.search(read_text(CABAL_FILE))[2]
|
||||
|
||||
|
||||
def changed_since(base: str, sources: list[str]) -> bool:
|
||||
@@ -378,13 +491,9 @@ def changes(base: str) -> None:
|
||||
|
||||
|
||||
def print_build_plan(args: argparse.Namespace) -> None:
|
||||
pull_request = args.event == "pull_request"
|
||||
result = build_plan(
|
||||
args.event,
|
||||
release_missing=args.release_missing,
|
||||
libs_release=args.libs_release,
|
||||
core_changed=pull_request and changed_since(args.base, CORE_SOURCES),
|
||||
addon_changed=pull_request and changed_since(args.base, ADDON_SOURCES),
|
||||
core_changed=changed_since(args.base, CORE_SOURCES),
|
||||
addon_changed=changed_since(args.base, ADDON_SOURCES),
|
||||
)
|
||||
print(f"platforms={build_matrix(result.platforms)}")
|
||||
print(f"test={'true' if result.test else ''}")
|
||||
@@ -429,10 +538,13 @@ def http_json(url: str, headers: dict[str, str] | None = None, method: str = "GE
|
||||
|
||||
|
||||
# Workflows pass unset repository variables as empty strings.
|
||||
def npm_published(name: str, version: str) -> bool:
|
||||
def npm_package_url(name: str) -> str:
|
||||
registry = os.environ.get("NPM_REGISTRY") or "https://registry.npmjs.org"
|
||||
package = registry_name(name, os.environ.get("NPM_SCOPE")).replace("/", "%2F")
|
||||
return http_json(f"{registry}/{package}/{version}") is not None
|
||||
return f"{registry}/{registry_name(name, os.environ.get('NPM_SCOPE')).replace('/', '%2F')}"
|
||||
|
||||
|
||||
def npm_published(name: str, version: str) -> bool:
|
||||
return http_json(f"{npm_package_url(name)}/{version}") is not None
|
||||
|
||||
|
||||
def python_project() -> str:
|
||||
@@ -445,19 +557,50 @@ def pypi_published(version: str) -> bool:
|
||||
return http_json(f"{index}/pypi/{project}/{version}/json") is not None
|
||||
|
||||
|
||||
def libs_repository() -> str:
|
||||
return os.environ.get("LIBS_REPOSITORY") or "simplex-chat/simplex-chat-libs"
|
||||
|
||||
|
||||
def libs_api(path: str, method: str = "GET", body: dict | None = None) -> Any:
|
||||
"""GitHub API of the simplex-chat-libs repository."""
|
||||
repo = os.environ.get("LIBS_REPOSITORY") or "simplex-chat/simplex-chat-libs"
|
||||
headers = {"Accept": "application/vnd.github+json"}
|
||||
if token := os.environ.get("GH_TOKEN"):
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
return http_json(f"https://api.github.com/repos/{repo}/{path}", headers, method, body)
|
||||
return http_json(f"https://api.github.com/repos/{libs_repository()}/{path}", headers, method, body)
|
||||
|
||||
|
||||
def github_release(tag: str) -> dict | None:
|
||||
"""The published release of the tag."""
|
||||
return libs_api(f"releases/tags/{tag}")
|
||||
|
||||
|
||||
def libs_releases() -> list[dict]:
|
||||
"""The releases of simplex-chat-libs; drafts are listed only with a token that can push to it."""
|
||||
releases: list[dict] = []
|
||||
page = 1
|
||||
while batch := libs_api(f"releases?per_page=100&page={page}"):
|
||||
releases += batch
|
||||
page += 1
|
||||
return releases
|
||||
|
||||
|
||||
def download(tag: str, asset: str, directory: str, sha256: str) -> Path:
|
||||
"""Saves the asset of the release to the directory; exits when its SHA-256 differs."""
|
||||
url = f"https://github.com/{libs_repository()}/releases/download/{tag}/{asset}"
|
||||
target = Path(directory) / asset
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
digest = hashlib.sha256()
|
||||
request = urllib.request.Request(url, headers={"User-Agent": "simplex-chat-release"})
|
||||
with urllib.request.urlopen(request, timeout=600) as response, target.open("wb") as file:
|
||||
while chunk := response.read(DOWNLOAD_CHUNK):
|
||||
file.write(chunk)
|
||||
digest.update(chunk)
|
||||
if digest.hexdigest() != sha256:
|
||||
target.unlink()
|
||||
sys.exit(f"SHA-256 of {asset} of {tag} is {digest.hexdigest()}, expected {sha256}")
|
||||
return target
|
||||
|
||||
|
||||
def read_releases() -> tuple[dict, str | None]:
|
||||
"""The releases and the blob of the file, which a change must name."""
|
||||
file = libs_api(f"contents/{RELEASES_FILE}")
|
||||
@@ -495,6 +638,13 @@ def commit_exists(commit: str) -> bool:
|
||||
return found.returncode == 0
|
||||
|
||||
|
||||
def fetch_commit(commit: str) -> str:
|
||||
"""The commit, fetched from origin when the checkout lacks it."""
|
||||
if not commit_exists(commit):
|
||||
git("fetch", "-q", "--depth=1", "origin", commit)
|
||||
return commit
|
||||
|
||||
|
||||
def npm_key(directory: str) -> str:
|
||||
return f"npm/{read_json(f'{directory}/package.json')['name']}"
|
||||
|
||||
@@ -517,21 +667,113 @@ def npm_package(directory: str, releases: dict) -> Package:
|
||||
)
|
||||
|
||||
|
||||
def prepare(args: argparse.Namespace) -> None:
|
||||
cabal_version = read_field("simplex-chat.cabal", r"^version:\s*(\S+)")
|
||||
libs_tag = f"core-{cabal_version}"
|
||||
addon = current_addon_tag()
|
||||
python_version, python_pins = parse_python_version(read_text(PYTHON_VERSION_FILE))
|
||||
def types_changed(releases: dict, branch: str) -> bool:
|
||||
"""Whether the types sources differ from those of the previous core release on the branch."""
|
||||
previous = previous_core_commit(releases, branch)
|
||||
return previous is None or changed_since(fetch_commit(previous), TYPES_SOURCES)
|
||||
|
||||
|
||||
def types_problem(version: str, releases: dict, branch: str) -> str | None:
|
||||
"""Why the types can't be released with core <version>, or None when they can."""
|
||||
if not types_changed(releases, branch):
|
||||
return None
|
||||
types = read_json(f"{TYPES_DIR}/package.json")
|
||||
if recorded_commit(releases, npm_key(TYPES_DIR), types["version"]) or npm_published(types["name"], types["version"]):
|
||||
return f"{types['name']} changed, and its version {types['version']} is published"
|
||||
if types.get("simplexChatCore") != version:
|
||||
return f"simplexChatCore of {types['name']} is {types.get('simplexChatCore')}, expected {version}"
|
||||
return None
|
||||
|
||||
|
||||
def claim_core(version: str, commit: str, branch: str) -> None:
|
||||
releases, _ = read_releases()
|
||||
highest = highest_recorded(releases, core_key(branch))
|
||||
if highest and version_key(version) < version_key(highest):
|
||||
sys.exit(f"core {version} is lower than core {highest} recorded for {branch}")
|
||||
if recorded := recorded_core_commit(releases, version):
|
||||
print(f"claim={'resume' if recorded == commit else 'skip'}")
|
||||
return
|
||||
if problem := types_problem(version, releases, branch):
|
||||
sys.exit(problem)
|
||||
libs_commit = record_releases([(core_key(branch), version)], commit)
|
||||
releases, _ = read_releases()
|
||||
if recorded_core_commit(releases, version) == commit:
|
||||
print("claim=new")
|
||||
print(f"commit={libs_commit}")
|
||||
else:
|
||||
print("claim=skip")
|
||||
|
||||
|
||||
def current_branch() -> str:
|
||||
return git("rev-parse", "--abbrev-ref", "HEAD")
|
||||
|
||||
|
||||
def core_release(version: str, branch: str, types_version: str | None) -> None:
|
||||
releases, _ = read_releases()
|
||||
highest = highest_recorded(releases, core_key(branch))
|
||||
if highest and version_key(version) <= version_key(highest):
|
||||
sys.exit(f"core {version} is not higher than core {highest} recorded for {branch}")
|
||||
(ROOT / CABAL_FILE).write_text(CABAL_VERSION.sub(rf"\g<1>{version}", read_text(CABAL_FILE), count=1))
|
||||
print(f"{CABAL_FILE}: {version}")
|
||||
if not types_version and not types_changed(releases, branch):
|
||||
return
|
||||
types = read_json(f"{TYPES_DIR}/package.json")
|
||||
if not types_version:
|
||||
previous = previous_core_commit(releases, branch)
|
||||
released = json.loads(git("show", f"{previous}:{TYPES_DIR}/package.json")) if previous else types
|
||||
types_version = bump_npm_version(released["version"])
|
||||
readme, found = TYPES_README_CORE.subn(f"SimpleX Chat core: {version}", read_text(TYPES_README), count=1)
|
||||
if not found:
|
||||
sys.exit(f"{TYPES_README} has no line 'SimpleX Chat core: <version>'")
|
||||
write_json(f"{TYPES_DIR}/package.json", {**types, "version": types_version, "simplexChatCore": version})
|
||||
(ROOT / TYPES_README).write_text(readme)
|
||||
print(f"{types['name']}: {types_version}")
|
||||
|
||||
|
||||
def pinned_tags(pins: Pins) -> list[str]:
|
||||
return [tag for tag in (pins.libs_release, pins.addon_release) if tag]
|
||||
|
||||
|
||||
def published_pin_problems(pins: Pins) -> list[str]:
|
||||
return pin_problems(pins, {tag: github_release(tag) for tag in pinned_tags(pins)})
|
||||
|
||||
|
||||
def prepare(args: argparse.Namespace) -> None:
|
||||
packages = frozenset(args.packages or ["nodejs", "python"])
|
||||
branch = args.branch or current_branch()
|
||||
releases, _ = read_releases()
|
||||
libs = libs_releases()
|
||||
libs_tag = latest_libs(libs, recorded_versions(releases, core_key(branch)))
|
||||
if libs_tag is None:
|
||||
sys.exit(f"no published library release recorded for {branch}")
|
||||
core = core_version(libs_tag)
|
||||
if version_key(cabal_version()) > version_key(core):
|
||||
print(f"warning: core-{cabal_version()} is not published, {libs_tag} is pinned", file=sys.stderr)
|
||||
sources_hash = current_addon_hash()
|
||||
addon = addon_release(libs, sources_hash)
|
||||
addon_digests = release_digests(addon, ADDON_ASSETS) if release_status(addon) == "published" else None
|
||||
if "nodejs" in packages and addon_digests is None:
|
||||
sys.exit(f"the add-on release of sources {sources_hash} is not published with every asset")
|
||||
types = read_json(f"{TYPES_DIR}/package.json")
|
||||
types_name = types["name"]
|
||||
if "nodejs" in packages and not npm_published(types_name, types["version"]):
|
||||
sys.exit(f"{types_name} {types['version']} is not published")
|
||||
if "python" in packages:
|
||||
core_commit = recorded_commit(releases, core_key(branch), core)
|
||||
if changed_since(core_commit, PYTHON_TYPES_SOURCES):
|
||||
sys.exit(f"the Python types differ from those of {libs_tag}")
|
||||
nodejs = read_json(f"{NODEJS_DIR}/package.json")
|
||||
python_version, python_pins = parse_python_version(read_text(PYTHON_VERSION_FILE))
|
||||
result = plan(
|
||||
State(
|
||||
libs_tag=libs_tag,
|
||||
libs_digests=release_digests(github_release(libs_tag), LIBS_ASSETS),
|
||||
addon_tag=addon,
|
||||
addon_digests=release_digests(github_release(addon), ADDON_ASSETS),
|
||||
libs_digests=release_digests(tag_release(libs, libs_tag), LIBS_ASSETS),
|
||||
addon_tag=addon["tag_name"] if addon else "",
|
||||
addon_digests=addon_digests,
|
||||
nodejs_pins=parse_release(read_text(RELEASE_FILE)),
|
||||
python_pins=python_pins,
|
||||
types=npm_package(TYPES_DIR, releases),
|
||||
types_dependency=f"^{types['version']}",
|
||||
nodejs_types_dependency=nodejs["dependencies"][types_name],
|
||||
nodejs=npm_package(NODEJS_DIR, releases),
|
||||
python=package_state(
|
||||
pypi_key(),
|
||||
@@ -542,24 +784,35 @@ def prepare(args: argparse.Namespace) -> None:
|
||||
releases,
|
||||
lambda: pypi_published(python_version),
|
||||
),
|
||||
types_version=args.types_version,
|
||||
packages=packages,
|
||||
nodejs_version=args.nodejs_version,
|
||||
python_version=args.python_version,
|
||||
)
|
||||
)
|
||||
(ROOT / RELEASE_FILE).write_text(render_release(result.nodejs_pins))
|
||||
(ROOT / PYTHON_VERSION_FILE).write_text(render_python_version(result.python_version, result.python_pins))
|
||||
types = read_json(f"{TYPES_DIR}/package.json")
|
||||
write_json(f"{TYPES_DIR}/package.json", {**types, "version": result.types_version})
|
||||
nodejs = read_json(f"{NODEJS_DIR}/package.json")
|
||||
dependencies = nodejs["dependencies"]
|
||||
if result.types_dependency:
|
||||
dependencies = {**dependencies, "@simplex-chat/types": result.types_dependency}
|
||||
write_json(f"{NODEJS_DIR}/package.json", {**nodejs, "version": result.nodejs_version, "dependencies": dependencies})
|
||||
summary = result.title + "\n\n" + "".join(f"- {note}\n" for note in result.notes)
|
||||
if args.summary:
|
||||
Path(args.summary).write_text(summary)
|
||||
print(summary, end="")
|
||||
selected_pins = [
|
||||
pins for name, pins in [("nodejs", result.nodejs_pins), ("python", result.python_pins)] if name in packages
|
||||
]
|
||||
if problems := [problem for pins in selected_pins for problem in published_pin_problems(pins)]:
|
||||
sys.exit("\n".join(problems))
|
||||
assets = {
|
||||
(tag, asset, pins.sha256[asset])
|
||||
for pins in selected_pins
|
||||
for tag in pinned_tags(pins)
|
||||
for asset in release_assets(tag)
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
for tag, asset, sha256 in sorted(assets):
|
||||
download(tag, asset, directory, sha256)
|
||||
print(f"checked {asset} of {tag}", file=sys.stderr)
|
||||
if "nodejs" in packages:
|
||||
(ROOT / RELEASE_FILE).write_text(render_release(result.nodejs_pins))
|
||||
dependencies = nodejs["dependencies"]
|
||||
if result.types_dependency:
|
||||
dependencies = {**dependencies, types_name: result.types_dependency}
|
||||
write_json(f"{NODEJS_DIR}/package.json", {**nodejs, "version": result.nodejs_version, "dependencies": dependencies})
|
||||
if "python" in packages:
|
||||
(ROOT / PYTHON_VERSION_FILE).write_text(render_python_version(result.python_version, result.python_pins))
|
||||
print(result.title + "\n\n" + "".join(f"- {note}\n" for note in result.notes), end="")
|
||||
|
||||
|
||||
def publish_plan() -> None:
|
||||
@@ -579,7 +832,7 @@ def publish_plan() -> None:
|
||||
|
||||
def record(args: argparse.Namespace) -> None:
|
||||
versions = [
|
||||
("core", args.core),
|
||||
(core_key(args.branch), args.core),
|
||||
(npm_key(TYPES_DIR), args.types),
|
||||
(npm_key(NODEJS_DIR), args.nodejs),
|
||||
(pypi_key(), args.python),
|
||||
@@ -617,35 +870,118 @@ def test_names() -> None:
|
||||
)
|
||||
|
||||
|
||||
def github_bool(value: str) -> bool:
|
||||
"""GitHub expressions pass booleans to the shell as "true", "false" or ""."""
|
||||
return value == "true"
|
||||
def print_release_status(tag: str) -> None:
|
||||
print(f"status={release_status(tag_release(libs_releases(), tag))}")
|
||||
|
||||
|
||||
def print_addon_release() -> None:
|
||||
sources_hash = current_addon_hash()
|
||||
release = addon_release(libs_releases(), sources_hash)
|
||||
print(f"notes={shlex.quote(addon_notes(sources_hash))}")
|
||||
print(f"status={release_status(release)}")
|
||||
print(f"tag={release['tag_name'] if release else ''}")
|
||||
|
||||
|
||||
def publish_release(tag: str) -> None:
|
||||
release = tag_release(libs_releases(), tag)
|
||||
if release is None:
|
||||
sys.exit(f"{tag}: no release")
|
||||
if not release["draft"]:
|
||||
print(f"{tag} is published")
|
||||
return
|
||||
if missing := missing_assets(release):
|
||||
sys.exit(f"{tag}: missing {', '.join(missing)}")
|
||||
libs_api(f"releases/{release['id']}", "PATCH", {"draft": False})
|
||||
print(f"{tag} published")
|
||||
|
||||
|
||||
def check_pins() -> None:
|
||||
_, python_pins = parse_python_version(read_text(PYTHON_VERSION_FILE))
|
||||
problems = published_pin_problems(parse_release(read_text(RELEASE_FILE))) + published_pin_problems(python_pins)
|
||||
if problems:
|
||||
sys.exit("\n".join(problems))
|
||||
print("the pins match the published releases")
|
||||
|
||||
|
||||
def download_asset(tag: str, asset: str, directory: str) -> None:
|
||||
digests = release_digests(github_release(tag), [asset])
|
||||
if digests is None:
|
||||
sys.exit(f"{asset} of {tag} is not published with a digest")
|
||||
download(tag, asset, directory, digests[asset])
|
||||
|
||||
|
||||
def download_pinned(asset: str, directory: str) -> None:
|
||||
pins = parse_release(read_text(RELEASE_FILE))
|
||||
tag = next((tag for tag in pinned_tags(pins) if asset in release_assets(tag)), None)
|
||||
if tag is None or asset not in pins.sha256:
|
||||
sys.exit(f"{asset} is not pinned in {RELEASE_FILE}")
|
||||
download(tag, asset, directory, pins.sha256[asset])
|
||||
|
||||
|
||||
def main(argv: list[str]) -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
commands = parser.add_subparsers(dest="command", required=True)
|
||||
commands.add_parser("addon-tag")
|
||||
branch_help = "the branch of the core versions, the checked out branch by default"
|
||||
core_release_parser = commands.add_parser("core-release")
|
||||
core_release_parser.add_argument("version", help="the new core version")
|
||||
core_release_parser.add_argument("--branch", help=branch_help)
|
||||
core_release_parser.add_argument("--types-version", help="this types version instead of a bump when needed")
|
||||
claim_parser = commands.add_parser("claim-core")
|
||||
claim_parser.add_argument("version", help="the core version of simplex-chat.cabal")
|
||||
claim_parser.add_argument("commit", help="the simplex-chat commit to release it from")
|
||||
claim_parser.add_argument("branch", help="the branch of the commit")
|
||||
status_parser = commands.add_parser("release-status")
|
||||
status_parser.add_argument("tag")
|
||||
commands.add_parser("addon-release")
|
||||
latest_libs_parser = commands.add_parser("latest-libs")
|
||||
latest_libs_parser.add_argument("--branch", help=branch_help)
|
||||
publish_release_parser = commands.add_parser("publish-release")
|
||||
publish_release_parser.add_argument("tag")
|
||||
commands.add_parser("check-pins")
|
||||
download_parser = commands.add_parser("download")
|
||||
download_parser.add_argument("tag")
|
||||
download_parser.add_argument("asset")
|
||||
download_parser.add_argument("directory")
|
||||
download_pinned_parser = commands.add_parser("download-pinned")
|
||||
download_pinned_parser.add_argument("asset")
|
||||
download_pinned_parser.add_argument("directory")
|
||||
changes_parser = commands.add_parser("changes")
|
||||
changes_parser.add_argument("base", help="the commit to compare with")
|
||||
build_parser = commands.add_parser("build-plan")
|
||||
build_parser.add_argument("--event", required=True, help="github.event_name")
|
||||
build_parser.add_argument("--base", default="", help="the base commit of a pull request")
|
||||
build_parser.add_argument("--release-missing", type=github_bool, default=False)
|
||||
build_parser.add_argument("--libs-release", default="")
|
||||
build_parser.add_argument("--base", required=True, help="the base commit of the pull request")
|
||||
prepare_parser = commands.add_parser("prepare")
|
||||
for package in ["nodejs", "types", "python"]:
|
||||
prepare_parser.add_argument("packages", nargs="*", choices=["nodejs", "python"], help="all when none is listed")
|
||||
prepare_parser.add_argument("--branch", help=branch_help)
|
||||
for package in ["nodejs", "python"]:
|
||||
prepare_parser.add_argument(f"--{package}-version", help="this version instead of a bump when needed")
|
||||
prepare_parser.add_argument("--summary", help="file for the PR title and body")
|
||||
commands.add_parser("publish-plan")
|
||||
record_parser = commands.add_parser("record")
|
||||
record_parser.add_argument("--commit", required=True, help="the simplex-chat commit released or published from")
|
||||
record_parser.add_argument("--branch", default="master", help="the branch of the core version")
|
||||
for name in ["core", "types", "nodejs", "python"]:
|
||||
record_parser.add_argument(f"--{name}", default="", help=f"{name} version, recorded when not empty")
|
||||
commands.add_parser("test-names")
|
||||
args = parser.parse_args(argv)
|
||||
if args.command == "addon-tag":
|
||||
print(current_addon_tag())
|
||||
if args.command == "core-release":
|
||||
core_release(args.version, args.branch or current_branch(), args.types_version)
|
||||
elif args.command == "claim-core":
|
||||
claim_core(args.version, args.commit, args.branch)
|
||||
elif args.command == "release-status":
|
||||
print_release_status(args.tag)
|
||||
elif args.command == "addon-release":
|
||||
print_addon_release()
|
||||
elif args.command == "latest-libs":
|
||||
releases, _ = read_releases()
|
||||
versions = recorded_versions(releases, core_key(args.branch or current_branch()))
|
||||
print(f"tag={latest_libs(libs_releases(), versions) or ''}")
|
||||
elif args.command == "publish-release":
|
||||
publish_release(args.tag)
|
||||
elif args.command == "check-pins":
|
||||
check_pins()
|
||||
elif args.command == "download":
|
||||
download_asset(args.tag, args.asset, args.directory)
|
||||
elif args.command == "download-pinned":
|
||||
download_pinned(args.asset, args.directory)
|
||||
elif args.command == "changes":
|
||||
changes(args.base)
|
||||
elif args.command == "build-plan":
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import argparse
|
||||
import base64
|
||||
import contextlib
|
||||
import email.message
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import subprocess
|
||||
import tomllib
|
||||
import urllib.error
|
||||
@@ -14,26 +17,41 @@ from unittest import mock
|
||||
import packages_release
|
||||
from packages_release import (
|
||||
ADDON_ASSETS,
|
||||
CABAL_FILE,
|
||||
NODEJS_DIR,
|
||||
PYTHON_DIR,
|
||||
PYTHON_VERSION_FILE,
|
||||
RELEASE_FILE,
|
||||
TYPES_DIR,
|
||||
TYPES_README,
|
||||
ADDON_PLATFORMS,
|
||||
BuildPlan,
|
||||
LIBS_ASSETS,
|
||||
Package,
|
||||
Pins,
|
||||
State,
|
||||
addon_tag,
|
||||
addon_hash,
|
||||
addon_notes,
|
||||
addon_release,
|
||||
build_matrix,
|
||||
build_plan,
|
||||
bump_npm_version,
|
||||
bump_python_version,
|
||||
changed_since,
|
||||
add_releases,
|
||||
claim_core,
|
||||
core_release,
|
||||
download,
|
||||
highest_recorded,
|
||||
latest_libs,
|
||||
missing_assets,
|
||||
package_state,
|
||||
pin_problems,
|
||||
prepare,
|
||||
publish_plan,
|
||||
record_releases,
|
||||
recorded_commit,
|
||||
release_status,
|
||||
render_releases,
|
||||
dist_tag,
|
||||
npm_published,
|
||||
@@ -45,6 +63,8 @@ from packages_release import (
|
||||
release_digests,
|
||||
render_python_version,
|
||||
render_release,
|
||||
tag_release,
|
||||
types_problem,
|
||||
)
|
||||
|
||||
|
||||
@@ -60,6 +80,10 @@ def github_release(assets: list[str], n: int) -> dict:
|
||||
return {"assets": [{"name": name, "digest": f"sha256:{sha(n)}"} for name in assets]}
|
||||
|
||||
|
||||
def listed_release(tag: str, assets: list[str], n: int, draft: bool = False, body: str | None = None) -> dict:
|
||||
return {"id": n, "tag_name": tag, "draft": draft, "body": body, **github_release(assets, n)}
|
||||
|
||||
|
||||
def state(**overrides) -> State:
|
||||
current = State(
|
||||
libs_tag="core-1.0.0.1",
|
||||
@@ -68,7 +92,8 @@ def state(**overrides) -> State:
|
||||
addon_digests=digests(ADDON_ASSETS, 2),
|
||||
nodejs_pins=Pins("core-1.0.0.1", {**digests(LIBS_ASSETS, 1), **digests(ADDON_ASSETS, 2)}, "nodejs-addon-aaa"),
|
||||
python_pins=Pins("core-1.0.0.1", digests(LIBS_ASSETS, 1)),
|
||||
types=Package("0.12.1", changed=False, published=True),
|
||||
types_dependency="^0.12.1",
|
||||
nodejs_types_dependency="^0.12.1",
|
||||
nodejs=Package("7.1.0-beta.6.1", changed=False, published=True),
|
||||
python=Package("7.1.0b6", changed=False, published=True),
|
||||
)
|
||||
@@ -101,15 +126,15 @@ class VersionTest(unittest.TestCase):
|
||||
self.assertEqual(registry_name("simplex-chat", None), "simplex-chat")
|
||||
|
||||
|
||||
class AddonTagTest(unittest.TestCase):
|
||||
class AddonHashTest(unittest.TestCase):
|
||||
def test_depends_on_sources_and_node_addon_api(self):
|
||||
files = {"a.cc": b"1", "b.h": b"2"}
|
||||
tag = addon_tag(files, "^8.5.0")
|
||||
self.assertRegex(tag, r"^nodejs-addon-[0-9a-f]{12}$")
|
||||
self.assertEqual(addon_tag(dict(reversed(files.items())), "^8.5.0"), tag)
|
||||
self.assertNotEqual(addon_tag({**files, "b.h": b"3"}, "^8.5.0"), tag)
|
||||
self.assertNotEqual(addon_tag({"a.cc": b"1", "c.h": b"2"}, "^8.5.0"), tag)
|
||||
self.assertNotEqual(addon_tag(files, "^8.6.0"), tag)
|
||||
sources_hash = addon_hash(files, "^8.5.0")
|
||||
self.assertRegex(sources_hash, r"^[0-9a-f]{12}$")
|
||||
self.assertEqual(addon_hash(dict(reversed(files.items())), "^8.5.0"), sources_hash)
|
||||
self.assertNotEqual(addon_hash({**files, "b.h": b"3"}, "^8.5.0"), sources_hash)
|
||||
self.assertNotEqual(addon_hash({"a.cc": b"1", "c.h": b"2"}, "^8.5.0"), sources_hash)
|
||||
self.assertNotEqual(addon_hash(files, "^8.6.0"), sources_hash)
|
||||
|
||||
|
||||
class PinFilesTest(unittest.TestCase):
|
||||
@@ -135,7 +160,7 @@ class ReleaseDigestsTest(unittest.TestCase):
|
||||
class PlanTest(unittest.TestCase):
|
||||
def test_nothing_changes_when_current(self):
|
||||
result = plan(state())
|
||||
self.assertEqual((result.nodejs_changed, result.python_changed, result.types_bumped), (False, False, False))
|
||||
self.assertEqual((result.nodejs_changed, result.python_changed, result.types_dependency), (False, False, None))
|
||||
self.assertEqual(result.notes, [])
|
||||
|
||||
def test_new_libs_release_updates_both_libraries(self):
|
||||
@@ -170,17 +195,17 @@ class PlanTest(unittest.TestCase):
|
||||
plan(state(python=Package("7.1.0b6", True, True))).title, "packages: release python 7.1.0b6.post1"
|
||||
)
|
||||
|
||||
def test_types_bump_updates_the_nodejs_dependency(self):
|
||||
result = plan(state(types=Package("0.12.1", True, True)))
|
||||
def test_new_types_version_updates_the_nodejs_dependency(self):
|
||||
result = plan(state(types_dependency="^0.12.2"))
|
||||
self.assertEqual(result.types_dependency, "^0.12.2")
|
||||
self.assertEqual(result.title, "packages: release types 0.12.2, nodejs 7.1.0-beta.6.2")
|
||||
self.assertEqual(result.title, "packages: release nodejs 7.1.0-beta.6.2")
|
||||
|
||||
def test_all_changes_combine_into_one_release(self):
|
||||
result = plan(
|
||||
state(
|
||||
**NEW_LIBS,
|
||||
**NEW_ADDON,
|
||||
types=Package("0.12.1", True, True),
|
||||
types_dependency="^0.12.2",
|
||||
nodejs=Package("7.1.0-beta.6.1", True, True),
|
||||
python=Package("7.1.0b6", True, True),
|
||||
)
|
||||
@@ -188,7 +213,16 @@ class PlanTest(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
(result.nodejs_pins.libs_release, result.nodejs_pins.addon_release), ("core-1.0.0.2", "nodejs-addon-bbb")
|
||||
)
|
||||
self.assertEqual(result.title, "packages: release types 0.12.2, nodejs 7.1.0-beta.6.2, python 7.1.0b6.post1")
|
||||
self.assertEqual(result.title, "packages: release nodejs 7.1.0-beta.6.2, python 7.1.0b6.post1")
|
||||
|
||||
def test_unlisted_package_keeps_its_pins_and_version(self):
|
||||
result = plan(state(**NEW_LIBS, types_dependency="^0.12.2", packages=frozenset({"python"})))
|
||||
self.assertEqual(result.nodejs_pins, state().nodejs_pins)
|
||||
self.assertEqual(
|
||||
(result.nodejs_version, result.nodejs_changed, result.types_dependency), ("7.1.0-beta.6.1", False, None)
|
||||
)
|
||||
self.assertEqual(result.python_pins.libs_release, "core-1.0.0.2")
|
||||
self.assertEqual(result.title, "packages: release python 7.1.0b6.post1")
|
||||
|
||||
def test_unpublished_versions_are_not_bumped_again(self):
|
||||
result = plan(
|
||||
@@ -199,29 +233,18 @@ class PlanTest(unittest.TestCase):
|
||||
self.assertEqual(result.title, "packages: release nodejs 7.1.0-beta.6.2, python 7.1.0b6.post1")
|
||||
|
||||
def test_requested_versions_win(self):
|
||||
result = plan(state(nodejs_version="7.2.0", types_version="0.13.0", python_version="7.2.0"))
|
||||
self.assertEqual(result.types_dependency, "^0.13.0")
|
||||
self.assertEqual(result.title, "packages: release types 0.13.0, nodejs 7.2.0, python 7.2.0")
|
||||
result = plan(state(nodejs_version="7.2.0", python_version="7.2.0"))
|
||||
self.assertEqual(result.title, "packages: release nodejs 7.2.0, python 7.2.0")
|
||||
|
||||
|
||||
class BuildPlanTest(unittest.TestCase):
|
||||
def plan(self, event="push", release_missing=False, libs_release="", core=False, addon=False):
|
||||
return build_plan(event, release_missing, libs_release, core, addon)
|
||||
|
||||
def test_push_builds_a_missing_addon_release_and_tests_it(self):
|
||||
self.assertEqual(self.plan(release_missing=True), BuildPlan(ADDON_PLATFORMS, test=True))
|
||||
self.assertEqual(self.plan(), BuildPlan([], test=True))
|
||||
|
||||
def test_pull_request_tests_with_the_pinned_libs(self):
|
||||
self.assertEqual(self.plan("pull_request"), BuildPlan(ADDON_PLATFORMS, test=True))
|
||||
self.assertEqual(self.plan("pull_request", addon=True), BuildPlan(ADDON_PLATFORMS, test=True))
|
||||
self.assertEqual(build_plan(core_changed=False, addon_changed=False), BuildPlan(ADDON_PLATFORMS, test=True))
|
||||
self.assertEqual(build_plan(core_changed=False, addon_changed=True), BuildPlan(ADDON_PLATFORMS, test=True))
|
||||
|
||||
def test_core_change_leaves_tests_to_build_yml(self):
|
||||
self.assertEqual(self.plan("pull_request", core=True), BuildPlan([], test=False))
|
||||
self.assertEqual(self.plan("pull_request", core=True, addon=True), BuildPlan(ADDON_PLATFORMS, test=False))
|
||||
|
||||
def test_new_library_release_is_tested_on_every_platform(self):
|
||||
self.assertEqual(self.plan(libs_release="core-1.0.0.2"), BuildPlan(ADDON_PLATFORMS, test=True))
|
||||
self.assertEqual(build_plan(core_changed=True, addon_changed=False), BuildPlan([], test=False))
|
||||
self.assertEqual(build_plan(core_changed=True, addon_changed=True), BuildPlan(ADDON_PLATFORMS, test=False))
|
||||
|
||||
def test_matrix_names_the_runner_of_each_platform(self):
|
||||
self.assertEqual(
|
||||
@@ -297,6 +320,12 @@ class ReleasesFileTest(unittest.TestCase):
|
||||
self.assertIsNone(recorded_commit(self.RELEASES, "npm/simplex-chat", "7.1.0-beta.6.4"))
|
||||
self.assertIsNone(recorded_commit({}, "pypi/simplex-chat", "7.1.0b6"))
|
||||
|
||||
def test_highest_recorded_version_is_compared_numerically(self):
|
||||
releases = {"core": {"7.1.0.9": "a", "7.1.0.11": "b"}, "npm": {"@simplex-chat/types": {"0.9.0": "c"}}}
|
||||
self.assertEqual(highest_recorded(releases, "core"), "7.1.0.11")
|
||||
self.assertEqual(highest_recorded(releases, "npm/@simplex-chat/types"), "0.9.0")
|
||||
self.assertIsNone(highest_recorded(releases, "npm/simplex-chat"))
|
||||
|
||||
def test_adds_entries_and_keeps_recorded_commits(self):
|
||||
updated = add_releases(self.RELEASES, [("npm/simplex-chat", "7.1.0-beta.6.4"), ("core", "7.1.0.11")], "x")
|
||||
self.assertEqual(updated["npm"]["simplex-chat"], {"7.1.0-beta.6.3": "n1", "7.1.0-beta.6.4": "x"})
|
||||
@@ -355,13 +384,10 @@ class RecordReleasesTest(unittest.TestCase):
|
||||
self.assertEqual(record_releases([("core", "1")], "other"), "libs0")
|
||||
|
||||
|
||||
class PackageStateTest(unittest.TestCase):
|
||||
class GitRepoTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.repo = Path(tempfile.mkdtemp())
|
||||
self.git("init", "-q")
|
||||
self.write("pkg/version.txt", 'version = "1"\n')
|
||||
self.write("pkg/src.txt", "a")
|
||||
self.commit("set version")
|
||||
|
||||
def git(self, *args):
|
||||
subprocess.run(["git", "-c", "user.name=t", "-c", "user.email=t@t", *args], cwd=self.repo, check=True)
|
||||
@@ -374,6 +400,19 @@ class PackageStateTest(unittest.TestCase):
|
||||
self.git("add", "-A")
|
||||
self.git("commit", "-q", "-m", message)
|
||||
|
||||
def head(self):
|
||||
return subprocess.run(
|
||||
["git", "rev-parse", "HEAD"], cwd=self.repo, capture_output=True, text=True
|
||||
).stdout.strip()
|
||||
|
||||
|
||||
class PackageStateTest(GitRepoTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.write("pkg/version.txt", 'version = "1"\n')
|
||||
self.write("pkg/src.txt", "a")
|
||||
self.commit("set version")
|
||||
|
||||
def state(self, releases=None, registry_published=False):
|
||||
with mock.patch.object(packages_release, "ROOT", self.repo):
|
||||
return package_state(
|
||||
@@ -383,11 +422,6 @@ class PackageStateTest(unittest.TestCase):
|
||||
def changed(self):
|
||||
return self.state().changed
|
||||
|
||||
def head(self):
|
||||
return subprocess.run(
|
||||
["git", "rev-parse", "HEAD"], cwd=self.repo, capture_output=True, text=True
|
||||
).stdout.strip()
|
||||
|
||||
def test_recorded_version_is_published_and_compared_with_its_commit(self):
|
||||
self.write("pkg/src.txt", "b")
|
||||
self.commit("change published with version 1")
|
||||
@@ -467,5 +501,303 @@ class PackageStateTest(unittest.TestCase):
|
||||
self.assertFalse(self.changed())
|
||||
|
||||
|
||||
class ReleasesTest(unittest.TestCase):
|
||||
def test_latest_libs_is_the_highest_complete_published_version(self):
|
||||
releases = [
|
||||
listed_release("core-7.1.0.9", LIBS_ASSETS, 1),
|
||||
listed_release("core-7.1.0.11", LIBS_ASSETS, 2),
|
||||
listed_release("core-7.1.0.12", LIBS_ASSETS[1:], 3),
|
||||
listed_release("core-7.1.0.13", LIBS_ASSETS, 4, draft=True),
|
||||
listed_release("core-test-7.1.0.14", LIBS_ASSETS, 5),
|
||||
listed_release("nodejs-addon-1111111", ADDON_ASSETS, 6),
|
||||
]
|
||||
versions = {"7.1.0.9", "7.1.0.11", "7.1.0.12", "7.1.0.13", "test-7.1.0.14"}
|
||||
self.assertEqual(latest_libs(releases, versions), "core-7.1.0.11")
|
||||
self.assertEqual(latest_libs(releases, {"7.1.0.9"}), "core-7.1.0.9")
|
||||
self.assertIsNone(latest_libs(releases, {"7.0.0.12"}))
|
||||
|
||||
def test_published_release_is_preferred_to_a_draft(self):
|
||||
draft = listed_release("core-7.1.0.12", LIBS_ASSETS, 1, draft=True)
|
||||
published = listed_release("core-7.1.0.12", LIBS_ASSETS, 2)
|
||||
self.assertEqual(tag_release([draft, published], "core-7.1.0.12"), published)
|
||||
self.assertEqual(tag_release([draft], "core-7.1.0.12"), draft)
|
||||
statuses = [
|
||||
release_status(tag_release(releases, "core-7.1.0.12")) for releases in ([draft, published], [draft], [])
|
||||
]
|
||||
self.assertEqual(statuses, ["published", "draft", "absent"])
|
||||
|
||||
def test_addon_release_is_found_by_its_notes(self):
|
||||
other = listed_release("nodejs-addon-1111111", ADDON_ASSETS, 1, body=addon_notes("bbb"))
|
||||
draft = listed_release("nodejs-addon-2222222", ADDON_ASSETS, 2, draft=True, body=addon_notes("aaa"))
|
||||
published = listed_release("nodejs-addon-3333333", ADDON_ASSETS, 3, body=addon_notes("aaa"))
|
||||
self.assertEqual(addon_release([other, draft, published], "aaa"), published)
|
||||
self.assertEqual(addon_release([other, draft], "aaa"), draft)
|
||||
self.assertIsNone(addon_release([other], "aaa"))
|
||||
|
||||
def test_missing_assets_of_a_draft(self):
|
||||
self.assertEqual(missing_assets(listed_release("core-7.1.0.12", LIBS_ASSETS[1:], 1)), LIBS_ASSETS[:1])
|
||||
self.assertEqual(missing_assets(listed_release("nodejs-addon-1111111", ADDON_ASSETS, 1)), [])
|
||||
|
||||
def test_pin_problems_list_mismatches_and_unpublished_releases(self):
|
||||
pins = Pins("core-7.1.0.11", {**digests(LIBS_ASSETS, 1), **digests(ADDON_ASSETS, 2)}, "nodejs-addon-1111111")
|
||||
published = {
|
||||
"core-7.1.0.11": github_release(LIBS_ASSETS, 1),
|
||||
"nodejs-addon-1111111": github_release(ADDON_ASSETS, 2),
|
||||
}
|
||||
self.assertEqual(pin_problems(pins, published), [])
|
||||
self.assertEqual(
|
||||
len(pin_problems(pins, {**published, "core-7.1.0.11": github_release(LIBS_ASSETS, 3)})), len(LIBS_ASSETS)
|
||||
)
|
||||
self.assertEqual(
|
||||
pin_problems(pins, {**published, "nodejs-addon-1111111": None}), ["nodejs-addon-1111111 is not published"]
|
||||
)
|
||||
|
||||
|
||||
class PublishReleaseTest(unittest.TestCase):
|
||||
def publish(self, releases):
|
||||
api = mock.Mock()
|
||||
with (
|
||||
mock.patch.object(packages_release, "libs_releases", return_value=releases),
|
||||
mock.patch.object(packages_release, "libs_api", api),
|
||||
contextlib.redirect_stdout(io.StringIO()),
|
||||
):
|
||||
packages_release.publish_release("core-7.1.0.12")
|
||||
return api
|
||||
|
||||
def test_complete_draft_is_published(self):
|
||||
api = self.publish([listed_release("core-7.1.0.12", LIBS_ASSETS, 7, draft=True)])
|
||||
api.assert_called_once_with("releases/7", "PATCH", {"draft": False})
|
||||
|
||||
def test_draft_with_missing_assets_stays_a_draft(self):
|
||||
with self.assertRaises(SystemExit):
|
||||
self.publish([listed_release("core-7.1.0.12", LIBS_ASSETS[1:], 7, draft=True)])
|
||||
|
||||
def test_published_release_is_left_unchanged(self):
|
||||
self.publish([listed_release("core-7.1.0.12", LIBS_ASSETS, 7)]).assert_not_called()
|
||||
|
||||
|
||||
class ClaimCoreTest(unittest.TestCase):
|
||||
def claim(self, recorded, after_record=None, problem=None, version="7.1.0.12", branch="master"):
|
||||
reads = iter([(recorded, "blob"), (after_record or recorded, "blob")])
|
||||
self.record = mock.Mock(return_value="libs1")
|
||||
output = io.StringIO()
|
||||
with (
|
||||
mock.patch.object(packages_release, "read_releases", lambda: next(reads)),
|
||||
mock.patch.object(packages_release, "record_releases", self.record),
|
||||
mock.patch.object(packages_release, "types_problem", return_value=problem),
|
||||
contextlib.redirect_stdout(output),
|
||||
):
|
||||
claim_core(version, "sx1", branch)
|
||||
return output.getvalue().splitlines()
|
||||
|
||||
def test_lower_version_fails(self):
|
||||
with self.assertRaises(SystemExit):
|
||||
self.claim({"core": {"master": {"7.1.0.13": "sx0"}}})
|
||||
|
||||
def test_version_recorded_for_this_commit_resumes(self):
|
||||
self.assertEqual(self.claim({"core": {"master": {"7.1.0.12": "sx1"}}}), ["claim=resume"])
|
||||
|
||||
def test_version_recorded_for_another_commit_is_skipped(self):
|
||||
self.assertEqual(self.claim({"core": {"master": {"7.1.0.11": "sx0", "7.1.0.12": "sx0"}}}), ["claim=skip"])
|
||||
|
||||
def test_version_recorded_on_another_branch_is_skipped(self):
|
||||
recorded = {"core": {"master": {"7.1.0.11": "sx0"}, "stable": {"7.1.0.12": "sx2"}}}
|
||||
self.assertEqual(self.claim(recorded), ["claim=skip"])
|
||||
|
||||
def test_failed_types_check_fails_before_recording(self):
|
||||
with self.assertRaises(SystemExit):
|
||||
self.claim({"core": {"master": {"7.1.0.11": "sx0"}}}, problem="types changed")
|
||||
self.record.assert_not_called()
|
||||
|
||||
def test_unrecorded_version_is_claimed(self):
|
||||
claimed = {"core": {"master": {"7.1.0.11": "sx0", "7.1.0.12": "sx1"}}}
|
||||
self.assertEqual(self.claim({"core": {"master": {"7.1.0.11": "sx0"}}}, claimed), ["claim=new", "commit=libs1"])
|
||||
self.record.assert_called_once_with([("core/master", "7.1.0.12")], "sx1")
|
||||
|
||||
def test_stable_versions_are_compared_with_stable_only(self):
|
||||
recorded = {"core": {"master": {"7.1.0.11": "sx0"}, "stable": {"7.0.0.12": "sx3"}}}
|
||||
claimed = {"core": {**recorded["core"], "stable": {"7.0.0.12": "sx3", "7.0.0.13": "sx1"}}}
|
||||
self.assertEqual(
|
||||
self.claim(recorded, claimed, version="7.0.0.13", branch="stable"), ["claim=new", "commit=libs1"]
|
||||
)
|
||||
self.record.assert_called_once_with([("core/stable", "7.0.0.13")], "sx1")
|
||||
with self.assertRaises(SystemExit):
|
||||
self.claim(recorded, version="7.0.0.11", branch="stable")
|
||||
|
||||
def test_concurrent_claim_of_another_commit_is_skipped(self):
|
||||
claimed = {"core": {"master": {"7.1.0.11": "sx0", "7.1.0.12": "sx2"}}}
|
||||
self.assertEqual(self.claim({"core": {"master": {"7.1.0.11": "sx0"}}}, claimed), ["claim=skip"])
|
||||
|
||||
|
||||
class TypesProblemTest(unittest.TestCase):
|
||||
def problem(self, types, changed=True, published=False, releases=None):
|
||||
directory = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(directory.cleanup)
|
||||
root = Path(directory.name)
|
||||
(root / TYPES_DIR).mkdir(parents=True)
|
||||
(root / TYPES_DIR / "package.json").write_text(json.dumps({"name": "@simplex-chat/types", **types}))
|
||||
with (
|
||||
mock.patch.object(packages_release, "ROOT", root),
|
||||
mock.patch.object(packages_release, "types_changed", return_value=changed),
|
||||
mock.patch.object(packages_release, "npm_published", return_value=published),
|
||||
):
|
||||
return types_problem("7.1.0.12", releases or {}, "master")
|
||||
|
||||
def test_unchanged_types_pass(self):
|
||||
self.assertIsNone(self.problem({"version": "0.12.1", "simplexChatCore": "7.1.0.11"}, changed=False))
|
||||
|
||||
def test_changed_types_with_a_new_version_for_the_core_pass(self):
|
||||
self.assertIsNone(self.problem({"version": "0.12.2", "simplexChatCore": "7.1.0.12"}))
|
||||
|
||||
def test_changed_types_with_a_published_version_fail(self):
|
||||
self.assertIsNotNone(self.problem({"version": "0.12.1", "simplexChatCore": "7.1.0.12"}, published=True))
|
||||
recorded = {"npm": {"@simplex-chat/types": {"0.12.1": "c"}}}
|
||||
self.assertIsNotNone(self.problem({"version": "0.12.1", "simplexChatCore": "7.1.0.12"}, releases=recorded))
|
||||
|
||||
def test_changed_types_for_another_core_fail(self):
|
||||
self.assertIsNotNone(self.problem({"version": "0.12.2", "simplexChatCore": "7.1.0.11"}))
|
||||
|
||||
|
||||
class CoreReleaseTest(GitRepoTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.write(CABAL_FILE, "name: simplex-chat\nversion: 7.1.0.11\n")
|
||||
types = {"name": "@simplex-chat/types", "version": "0.12.1", "simplexChatCore": "7.1.0.11"}
|
||||
self.write(f"{TYPES_DIR}/package.json", json.dumps(types, indent=2) + "\n")
|
||||
self.write(TYPES_README, "# Types\n\nSimpleX Chat core: 7.1.0.11\n")
|
||||
self.write(f"{TYPES_DIR}/src/types.ts", "a")
|
||||
self.commit("types 0.12.1")
|
||||
self.releases = {"core": {"master": {"7.1.0.11": self.head()}}}
|
||||
|
||||
def release(self, version, types_version=None):
|
||||
with (
|
||||
mock.patch.object(packages_release, "ROOT", self.repo),
|
||||
mock.patch.object(packages_release, "read_releases", return_value=(self.releases, "blob")),
|
||||
contextlib.redirect_stdout(io.StringIO()),
|
||||
):
|
||||
core_release(version, "master", types_version)
|
||||
|
||||
def read(self, file):
|
||||
return (self.repo / file).read_text()
|
||||
|
||||
def types(self):
|
||||
return json.loads(self.read(f"{TYPES_DIR}/package.json"))
|
||||
|
||||
def test_changed_types_get_a_new_version_for_the_core(self):
|
||||
self.write(f"{TYPES_DIR}/src/types.ts", "b")
|
||||
self.commit("types change")
|
||||
self.release("7.1.0.12")
|
||||
self.assertIn("version: 7.1.0.12\n", self.read(CABAL_FILE))
|
||||
self.assertEqual((self.types()["version"], self.types()["simplexChatCore"]), ("0.12.2", "7.1.0.12"))
|
||||
self.assertIn("SimpleX Chat core: 7.1.0.12\n", self.read(TYPES_README))
|
||||
|
||||
def test_unchanged_types_keep_their_version(self):
|
||||
self.release("7.1.0.12")
|
||||
self.assertIn("version: 7.1.0.12\n", self.read(CABAL_FILE))
|
||||
self.assertEqual((self.types()["version"], self.types()["simplexChatCore"]), ("0.12.1", "7.1.0.11"))
|
||||
|
||||
def test_requested_types_version_wins(self):
|
||||
self.release("7.1.0.12", "7.1.0-beta.6.1")
|
||||
self.assertEqual((self.types()["version"], self.types()["simplexChatCore"]), ("7.1.0-beta.6.1", "7.1.0.12"))
|
||||
|
||||
def test_types_version_is_bumped_from_the_previous_core_release(self):
|
||||
self.write(f"{TYPES_DIR}/package.json", json.dumps({**self.types(), "version": "0.12.5"}))
|
||||
self.write(f"{TYPES_DIR}/src/types.ts", "b")
|
||||
self.commit("types change and a manual version")
|
||||
self.release("7.1.0.12")
|
||||
self.assertEqual(self.types()["version"], "0.12.2")
|
||||
|
||||
def test_version_must_be_higher_than_the_recorded_one(self):
|
||||
with self.assertRaises(SystemExit):
|
||||
self.release("7.1.0.11")
|
||||
self.assertIn("version: 7.1.0.11\n", self.read(CABAL_FILE))
|
||||
|
||||
|
||||
class PrepareTest(GitRepoTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
nodejs = {
|
||||
"name": "simplex-chat",
|
||||
"version": "7.1.0-beta.6.1",
|
||||
"dependencies": {"@simplex-chat/types": "^0.12.1"},
|
||||
"devDependencies": {"node-addon-api": "^8.5.0"},
|
||||
}
|
||||
self.write(CABAL_FILE, "version: 1.0.0.2\n")
|
||||
self.write(f"{NODEJS_DIR}/binding.gyp", "{}")
|
||||
self.write(f"{NODEJS_DIR}/cpp/simplex.cc", "add-on")
|
||||
self.write(f"{NODEJS_DIR}/package.json", json.dumps(nodejs))
|
||||
self.write(RELEASE_FILE, render_release(state().nodejs_pins))
|
||||
self.write(f"{TYPES_DIR}/package.json", json.dumps({"name": "@simplex-chat/types", "version": "0.12.1"}))
|
||||
self.write(PYTHON_VERSION_FILE, render_python_version("7.1.0b6", state().python_pins))
|
||||
self.write(f"{PYTHON_DIR}/src/simplex_chat/types/_types.py", "a")
|
||||
self.write(f"{PYTHON_DIR}/pyproject.toml", 'name = "simplex-chat"\n')
|
||||
self.commit("packages")
|
||||
with mock.patch.object(packages_release, "ROOT", self.repo):
|
||||
sources_hash = packages_release.current_addon_hash()
|
||||
self.libs = [
|
||||
listed_release("core-1.0.0.2", LIBS_ASSETS, 3),
|
||||
listed_release("nodejs-addon-1111111", ADDON_ASSETS, 4, body=addon_notes(sources_hash)),
|
||||
]
|
||||
self.releases = {
|
||||
"core": {"master": {"1.0.0.2": self.head()}},
|
||||
"npm": {"simplex-chat": {"7.1.0-beta.6.1": self.head()}},
|
||||
"pypi": {"simplex-chat": {"7.1.0b6": self.head()}},
|
||||
}
|
||||
|
||||
def prepare(self, *packages):
|
||||
published = {release["tag_name"]: release for release in self.libs}
|
||||
download = mock.Mock()
|
||||
with (
|
||||
mock.patch.object(packages_release, "ROOT", self.repo),
|
||||
mock.patch.object(packages_release, "read_releases", return_value=(self.releases, "blob")),
|
||||
mock.patch.object(packages_release, "libs_releases", return_value=self.libs),
|
||||
mock.patch.object(packages_release, "github_release", published.get),
|
||||
mock.patch.object(packages_release, "npm_published", return_value=True),
|
||||
mock.patch.object(packages_release, "pypi_published", return_value=True),
|
||||
mock.patch.object(packages_release, "download", download),
|
||||
contextlib.redirect_stdout(io.StringIO()),
|
||||
contextlib.redirect_stderr(io.StringIO()),
|
||||
):
|
||||
prepare(
|
||||
argparse.Namespace(packages=list(packages), branch="master", nodejs_version=None, python_version=None)
|
||||
)
|
||||
return download
|
||||
|
||||
def test_listed_package_is_released_alone(self):
|
||||
download = self.prepare("nodejs")
|
||||
pins = parse_release((self.repo / RELEASE_FILE).read_text())
|
||||
self.assertEqual((pins.libs_release, pins.addon_release), ("core-1.0.0.2", "nodejs-addon-1111111"))
|
||||
self.assertEqual(json.loads((self.repo / NODEJS_DIR / "package.json").read_text())["version"], "7.1.0-beta.6.2")
|
||||
python = parse_python_version((self.repo / PYTHON_VERSION_FILE).read_text())
|
||||
self.assertEqual(python, ("7.1.0b6", state().python_pins))
|
||||
self.assertEqual(download.call_count, len(LIBS_ASSETS) + len(ADDON_ASSETS))
|
||||
|
||||
def test_python_types_newer_than_the_pinned_core_stop_the_release(self):
|
||||
self.write(f"{PYTHON_DIR}/src/simplex_chat/types/_types.py", "b")
|
||||
self.commit("python types change")
|
||||
with self.assertRaises(SystemExit):
|
||||
self.prepare("python")
|
||||
self.assertEqual(parse_python_version((self.repo / PYTHON_VERSION_FILE).read_text())[0], "7.1.0b6")
|
||||
|
||||
|
||||
class DownloadTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
directory = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(directory.cleanup)
|
||||
self.directory = directory.name
|
||||
|
||||
def download(self, sha256):
|
||||
with mock.patch.object(packages_release.urllib.request, "urlopen", return_value=io.BytesIO(b"data")):
|
||||
return download("core-1.0.0.1", "a.zip", self.directory, sha256)
|
||||
|
||||
def test_matching_digest_keeps_the_file(self):
|
||||
self.assertEqual(self.download(hashlib.sha256(b"data").hexdigest()).read_bytes(), b"data")
|
||||
|
||||
def test_mismatching_digest_removes_the_file(self):
|
||||
with self.assertRaises(SystemExit):
|
||||
self.download(sha(1))
|
||||
self.assertFalse((Path(self.directory) / "a.zip").exists())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+1
-1
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplex-chat
|
||||
version: 7.1.0.11
|
||||
version: 7.1.0.12
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
|
||||
Reference in new issue
Block a user