diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..2e47f34 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"], + "dependencyDashboard": false, + "enabledManagers": ["mise"], + "schedule": ["before 6am on Tuesday"], + "minimumReleaseAge": "7 days", + "lockFileMaintenance": { + "enabled": true, + "schedule": ["before 6am on Tuesday"] + }, + "packageRules": [ + { + "description": "A JDK major bump has to move together with jvmToolchain in build.gradle.kts", + "matchManagers": ["mise"], + "matchPackageNames": ["java"], + "matchUpdateTypes": ["major"], + "enabled": false + } + ] +} diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index d19bd51..f32d1e1 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -14,84 +14,14 @@ concurrency: permissions: contents: read jobs: - build: - name: ๐Ÿ—๏ธ Build - runs-on: ubuntu-26.04 - steps: - - name: ๐Ÿงฝ Free disk space - uses: jlumbroso/free-disk-space@ceedf095f4ec1a097402bc6bd80831f2e1a6fde6 # v2.0.0 - with: - tool-cache: false - large-packages: false - - name: ๐Ÿ“ฅ Check out code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: ๐Ÿงฐ Set up mise (JDKs) - uses: jdx/mise-action@6be3c7ed4c21d4f76b208d351d95aac273ebb76d # v5.2.4 - - name: โ˜• Set up Gradle - uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 - - name: ๐Ÿ—๏ธ Build plugin - run: ./gradlew --console=plain buildPlugin - - name: โš™๏ธ Prepare artifact - id: artifact - working-directory: build/distributions - run: | - shopt -s nullglob - archives=(*.zip) - test "${#archives[@]}" -eq 1 - unzip "${archives[0]}" -d content - echo "filename=${archives[0]%.zip}" >>"$GITHUB_OUTPUT" - - name: ๐Ÿ“ค Upload artifact - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: ${{ steps.artifact.outputs.filename }} - path: ./build/distributions/content/*/* - verify: - name: ๐Ÿ” Verify (${{ matrix.label }}) - runs-on: ubuntu-26.04 - strategy: - fail-fast: false - matrix: - # The EAP entry catches platform APIs that the next release moves or removes. - include: - - label: PY - version: "" - - label: PY 263 EAP - version: 263-EAP-SNAPSHOT - steps: - - name: ๐Ÿงฝ Free disk space - uses: jlumbroso/free-disk-space@ceedf095f4ec1a097402bc6bd80831f2e1a6fde6 # v2.0.0 - with: - tool-cache: false - large-packages: false - android: true - dotnet: true - haskell: true - docker-images: true - - name: ๐Ÿ“ฅ Check out code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: ๐Ÿงฐ Set up mise (JDKs) - uses: jdx/mise-action@6be3c7ed4c21d4f76b208d351d95aac273ebb76d # v5.2.4 - - name: โ˜• Set up Gradle - uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 - - name: ๐Ÿ’พ Cache verifier IDEs - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ~/.pluginVerifier/ides - key: plugin-verifier-ides-${{ matrix.version }}-${{ hashFiles('gradle.properties') }} - - name: ๐Ÿ” Run verification - run: ./gradlew verifyPlugin -PverifyIde=PY ${MATRIX_VERSION:+-PverifyVersion=$MATRIX_VERSION} - env: - MATRIX_VERSION: ${{ matrix.version }} - - name: ๐Ÿ“ค Upload results - if: ${{ always() }} - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: pluginVerifier-result-${{ matrix.label }} - path: ${{ github.workspace }}/build/reports/pluginVerifier + plugin: + name: ๐Ÿงฉ JetBrains plugin check + uses: tox-dev/github-workflows/.github/workflows/jetbrains-plugin-check.yaml@2c1f5e4ceda37c41eb8367b5cbeaa741aa60a566 # 6.5.2 + permissions: + contents: read + with: + # The EAP entry catches platform APIs that the next release moves or removes. + verify-matrix: '[{"label":"PY","ide":"PY","version":""},{"label":"PY 263 EAP","ide":"PY","version":"263-EAP-SNAPSHOT"}]' lint: name: ๐Ÿงน Lint runs-on: ubuntu-26.04 @@ -130,51 +60,14 @@ jobs: uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 - name: ๐Ÿงช Run tests with coverage run: ./gradlew test koverVerify - ui-test: - name: ๐Ÿงช UI tests - runs-on: ubuntu-26.04 - steps: - - name: ๐Ÿงฝ Free disk space - uses: jlumbroso/free-disk-space@ceedf095f4ec1a097402bc6bd80831f2e1a6fde6 # v2.0.0 - with: - tool-cache: false - large-packages: false - - name: ๐Ÿ“ฅ Check out code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: ๐Ÿงฐ Set up mise (JDKs) - uses: jdx/mise-action@6be3c7ed4c21d4f76b208d351d95aac273ebb76d # v5.2.4 - - name: โ˜• Set up Gradle - uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 - - name: ๐Ÿงช Run UI tests - run: | - export DISPLAY=:99.0 - Xvfb :99 -screen 0 1920x1080x24 & - ./gradlew runIdeForUiTests & - echo "Waiting for IDE to start..." - timeout 180 bash -c 'until curl -s http://127.0.0.1:8082 > /dev/null 2>&1; do sleep 2; done' || { - echo "IDE failed to start" - exit 1 - } - echo "IDE is ready" - ./gradlew uiTest - kill %1 %2 || true - - name: ๐Ÿ“ค Upload UI test artifacts - if: ${{ failure() }} - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: ui-test-failure - path: | - build/reports/ - build/idea-sandbox/**/log/idea.log gate: - name: ๐Ÿšฆ Required checks pass + name: "๐Ÿšฆ ${{ github.event_name == 'pull_request' && 'Required checks pass' || 'Non-PR checks pass' }}" if: ${{ !cancelled() }} - needs: [build, verify, lint, test, ui-test] + needs: [plugin, lint, test] runs-on: ubuntu-26.04 + permissions: {} steps: - name: ๐Ÿšฆ Require all needed jobs to pass - env: - NEEDS_JSON: ${{ toJSON(needs) }} - run: jq -e 'all(to_entries[]; .value.result == "success")' <<<"$NEEDS_JSON" + uses: tox-dev/github-workflows/required-checks@2c1f5e4ceda37c41eb8367b5cbeaa741aa60a566 # 6.5.2 + with: + needs: ${{ toJSON(needs) }} diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml index d7df6f3..8c3e8c2 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -11,23 +11,22 @@ concurrency: cancel-in-progress: true permissions: {} jobs: - analyze: - name: ๐Ÿ›ก๏ธ Analyze ${{ matrix.language }} + actions: + name: ๐Ÿ›ก๏ธ Analyze + uses: tox-dev/github-workflows/.github/workflows/codeql.yaml@2c1f5e4ceda37c41eb8367b5cbeaa741aa60a566 # 6.5.2 + permissions: + contents: read + security-events: write + with: + languages: '["actions"]' + java-kotlin: + name: ๐Ÿ›ก๏ธ Analyze java-kotlin runs-on: ubuntu-26.04 permissions: contents: read security-events: write - strategy: - fail-fast: false - matrix: - include: - - language: actions - build-mode: none - - language: java-kotlin - build-mode: manual steps: - name: ๐Ÿงฝ Free disk space - if: matrix.build-mode == 'manual' uses: jlumbroso/free-disk-space@ceedf095f4ec1a097402bc6bd80831f2e1a6fde6 # v2.0.0 with: tool-cache: false @@ -39,20 +38,17 @@ jobs: - name: ๐Ÿ›ก๏ธ Initialize CodeQL uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} + languages: java-kotlin + build-mode: manual - name: ๐Ÿงฐ Set up mise (JDKs) - if: matrix.build-mode == 'manual' uses: jdx/mise-action@6be3c7ed4c21d4f76b208d351d95aac273ebb76d # v5.2.4 - name: โ˜• Set up Gradle - if: matrix.build-mode == 'manual' uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 - name: ๐Ÿ—๏ธ Compile plugin sources - if: matrix.build-mode == 'manual' # CodeQL extracts only what the compiler actually runs on, so a cache hit leaves the database empty; the # extractor runs inside the Kotlin compiler and overflows the default heap. run: ./gradlew --console=plain --no-build-cache --rerun-tasks -Dorg.gradle.jvmargs=-Xmx4g -Pkotlin.daemon.jvmargs=-Xmx6g compileKotlin compileJava - name: ๐Ÿ›ก๏ธ Analyze uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 with: - category: "/language:${{ matrix.language }}" + category: "/language:java-kotlin" diff --git a/.github/workflows/mise-update.yaml b/.github/workflows/mise-update.yaml deleted file mode 100644 index 14ec7b3..0000000 --- a/.github/workflows/mise-update.yaml +++ /dev/null @@ -1,162 +0,0 @@ -name: ๐Ÿ”’ Mise update -on: - schedule: - - cron: "0 9 * * 2" - workflow_dispatch: -permissions: - contents: read -concurrency: - group: mise-update - cancel-in-progress: false -jobs: - bump: - name: ๐Ÿ“ฆ Bump mise-managed JDKs - runs-on: ubuntu-26.04 - outputs: - changed: ${{ steps.changed.outputs.changed }} - digest: ${{ steps.digest.outputs.sha }} - base-commit: ${{ steps.base.outputs.commit }} - artifact-id: ${{ steps.upload.outputs.artifact-id }} - steps: - - name: ๐Ÿ“ฅ Check out code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: โš™๏ธ Record source commit - id: base - run: echo "commit=$(git rev-parse HEAD)" >>"$GITHUB_OUTPUT" - - name: ๐Ÿงฐ Set up mise - uses: jdx/mise-action@6be3c7ed4c21d4f76b208d351d95aac273ebb76d # v5.2.4 - with: - install: false - cache: false - - name: ๐Ÿ”’ Bump the lockfile - run: mise lock --bump - - name: ๐Ÿ”’ Check the lockfile - id: changed - run: | - if git diff --quiet -- mise.lock; then - echo 'changed=false' >>"$GITHUB_OUTPUT" - else - echo 'changed=true' >>"$GITHUB_OUTPUT" - fi - - name: โš™๏ธ Record lockfile digest - id: digest - if: steps.changed.outputs.changed == 'true' - run: echo "sha=$(sha256sum mise.lock | cut -d' ' -f1)" >>"$GITHUB_OUTPUT" - - name: ๐Ÿ“ค Store the lockfile - id: upload - if: steps.changed.outputs.changed == 'true' - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: mise-lock - path: mise.lock - prepare-pr: - name: โš™๏ธ Prepare lockfile PR - needs: bump - if: needs.bump.outputs.changed == 'true' - runs-on: ubuntu-26.04 - permissions: - contents: read - outputs: - base-commit: ${{ steps.change.outputs.base-commit }} - candidate-commit: ${{ steps.change.outputs.candidate-commit }} - branch: ${{ steps.branch.outputs.name }} - artifact-id: ${{ steps.upload.outputs.artifact-id }} - steps: - - name: ๐Ÿ“ฅ Check out code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ needs.bump.outputs.base-commit }} - fetch-depth: 0 - persist-credentials: false - - name: ๐Ÿ“ฉ Download the lockfile - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 - with: - artifact-ids: ${{ needs.bump.outputs.artifact-id }} - path: ${{ runner.temp }}/mise-lock - - name: ๐Ÿ” Verify the lockfile artifact - env: - EXPECTED_SHA: ${{ needs.bump.outputs.digest }} - run: | - test -f "$RUNNER_TEMP/mise-lock/mise.lock" - test ! -L "$RUNNER_TEMP/mise-lock/mise.lock" - read -r digest _ < <(sha256sum "$RUNNER_TEMP/mise-lock/mise.lock") - test "$digest" = "$EXPECTED_SHA" - - name: โš™๏ธ Select lockfile branch - id: branch - env: - DIGEST: ${{ needs.bump.outputs.digest }} - run: echo "name=mise-update-${DIGEST:0:12}" >>"$GITHUB_OUTPUT" - - name: โš™๏ธ Resolve the release App author - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - slug=tox-dev-pyvenvmanage-release - app_user_id=$(gh api "/users/${slug}[bot]" --jq .id) - git config user.name "${slug}[bot]" - git config user.email "${app_user_id}+${slug}[bot]@users.noreply.github.com" - - name: ๐Ÿท๏ธ Prepare the exact lockfile change - id: change - run: | - set -euo pipefail - mkdir -p "$RUNNER_TEMP/mise-pr" - base_commit=$(git rev-parse HEAD) - cp "$RUNNER_TEMP/mise-lock/mise.lock" mise.lock - git add mise.lock - printf 'mise.lock\t%s\n' "$(git rev-parse :mise.lock)" >"$RUNNER_TEMP/mise-pr/paths.tsv" - printf 'Refresh mise.lock within the versions pinned in mise.toml.\n' >"$RUNNER_TEMP/mise-pr/body.md" - candidate_commit= - if ! git diff --cached --quiet; then - git commit -m "Bump mise-managed JDKs" - candidate_commit=$(git rev-parse HEAD) - git branch release-candidate HEAD - git bundle create "$RUNNER_TEMP/mise-pr/candidate.bundle" refs/heads/release-candidate - fi - echo "base-commit=$base_commit" >>"$GITHUB_OUTPUT" - echo "candidate-commit=$candidate_commit" >>"$GITHUB_OUTPUT" - - name: ๐Ÿ“ค Upload the lockfile change - id: upload - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: mise-pr-${{ github.run_id }}-${{ github.run_attempt }} - path: ${{ runner.temp }}/mise-pr/* - if-no-files-found: error - open-pr: - name: ๐Ÿ”€ Open lockfile PR - needs: prepare-pr - runs-on: ubuntu-26.04 - environment: release-auth - permissions: - contents: read - steps: - - name: ๐Ÿ”‘ Create release App token - id: token - uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 - with: - client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} - private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} - owner: ${{ github.repository_owner }} - repositories: ${{ github.event.repository.name }} - permission-contents: write - permission-pull-requests: write - permission-issues: write - - name: ๐Ÿ“ฉ Download the lockfile change - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 - with: - artifact-ids: ${{ needs.prepare-pr.outputs.artifact-id }} - path: ${{ runner.temp }}/mise-pr - - name: ๐Ÿ”€ Open the lockfile PR - uses: tox-dev/github-workflows/publish-post-release-pr@57a765127aae73adf7d0e24a7fe0f067f2d3ea3e # 6.2.1 - env: - GH_TOKEN: ${{ steps.token.outputs.token }} - with: - base-commit: ${{ needs.prepare-pr.outputs.base-commit }} - branch: ${{ needs.prepare-pr.outputs.branch }} - title: Bump mise-managed JDKs - body-file: ${{ runner.temp }}/mise-pr/body.md - paths-file: ${{ runner.temp }}/mise-pr/paths.tsv - allowed-paths: mise.lock - bundle: ${{ needs.prepare-pr.outputs.candidate-commit != '' && format('{0}/mise-pr/candidate.bundle', runner.temp) || '' }} - candidate-commit: ${{ needs.prepare-pr.outputs.candidate-commit }} diff --git a/.github/workflows/news.yaml b/.github/workflows/news.yaml index 410491c..43dd82c 100644 --- a/.github/workflows/news.yaml +++ b/.github/workflows/news.yaml @@ -18,15 +18,15 @@ jobs: fetch-depth: 0 persist-credentials: false - name: ๐Ÿ Install uv - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: version: latest-known python-version: "3.15" enable-cache: false - name: ๐Ÿ“ฆ Install locked release tools - uses: tox-dev/github-workflows/install-locked-tools@99024bcb5749b178c5514f86acfcf1e80c865e75 # 6.3.0 + uses: tox-dev/github-workflows/install-locked-tools@2c1f5e4ceda37c41eb8367b5cbeaa741aa60a566 # 6.5.2 with: groups: release-news venv: ${{ runner.temp }}/release-tools - name: ๐Ÿ” Validate news fragments - uses: tox-dev/github-workflows/validate-news@99024bcb5749b178c5514f86acfcf1e80c865e75 # 6.3.0 + uses: tox-dev/github-workflows/validate-news@2c1f5e4ceda37c41eb8367b5cbeaa741aa60a566 # 6.5.2 diff --git a/.github/workflows/post-release.yaml b/.github/workflows/post-release.yaml deleted file mode 100644 index 35c530f..0000000 --- a/.github/workflows/post-release.yaml +++ /dev/null @@ -1,122 +0,0 @@ -name: ๐Ÿ”€ Post-release -on: - # The bump runs from main so release-auth can stay main-only; it checks out main and reads no tag run output. - workflow_run: # zizmor: ignore[dangerous-triggers] - workflows: ["๐Ÿš€ Release"] - types: [completed] -permissions: {} -concurrency: - group: post-release-${{ github.event.workflow_run.head_branch }} - cancel-in-progress: false -jobs: - post-release-prepare: - name: ๐Ÿท๏ธ Prepare post-release version bump - if: >- - github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && github.event.workflow_run.path == '.github/workflows/release.yaml' && github.event.workflow_run.head_repository.full_name == github.repository - runs-on: ubuntu-26.04 - permissions: - contents: read - outputs: - tag: ${{ steps.source.outputs.tag }} - base-commit: ${{ steps.change.outputs.base-commit }} - candidate-commit: ${{ steps.change.outputs.candidate-commit }} - branch: ${{ steps.version.outputs.branch }} - next-version: ${{ steps.version.outputs.next-version }} - artifact-id: ${{ steps.upload.outputs.artifact-id }} - steps: - - name: ๐Ÿ“ฅ Check out code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: main - fetch-depth: 0 - persist-credentials: false - - name: โš™๏ธ Record the released tag - id: source - env: - TAG: ${{ github.event.workflow_run.head_branch }} - run: echo "tag=$TAG" >>"$GITHUB_OUTPUT" - - name: ๐Ÿท๏ธ Select the next version - id: version - env: - TAG: ${{ steps.source.outputs.tag }} - run: | - set -euo pipefail - version="${TAG#v}" - IFS='.' read -r major minor patch extra <<<"$version" - echo "next-version=$major.$minor.$((patch + 1))-dev" >>"$GITHUB_OUTPUT" - echo "branch=post-release-$TAG" >>"$GITHUB_OUTPUT" - - name: โš™๏ธ Resolve the release App author - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - slug=tox-dev-pyvenvmanage-release - app_user_id=$(gh api "/users/${slug}[bot]" --jq .id) - git config user.name "${slug}[bot]" - git config user.email "${app_user_id}+${slug}[bot]@users.noreply.github.com" - - name: ๐Ÿท๏ธ Prepare the exact version change - id: change - env: - TAG: ${{ steps.source.outputs.tag }} - NEXT_VERSION: ${{ steps.version.outputs.next-version }} - run: | - set -euo pipefail - mkdir -p "$RUNNER_TEMP/post-release" - base_commit=$(git rev-parse HEAD) - sed -i "s/^pluginVersion=.*/pluginVersion=$NEXT_VERSION/" gradle.properties - git add gradle.properties - printf 'gradle.properties\t%s\n' "$(git rev-parse :gradle.properties)" >"$RUNNER_TEMP/post-release/paths.tsv" - printf 'Set the next development version after %s.\n' "$TAG" >"$RUNNER_TEMP/post-release/body.md" - candidate_commit= - if ! git diff --cached --quiet; then - git commit -m "Bump version to $NEXT_VERSION after $TAG" - candidate_commit=$(git rev-parse HEAD) - git branch release-candidate HEAD - git bundle create "$RUNNER_TEMP/post-release/candidate.bundle" refs/heads/release-candidate - fi - echo "base-commit=$base_commit" >>"$GITHUB_OUTPUT" - echo "candidate-commit=$candidate_commit" >>"$GITHUB_OUTPUT" - - name: ๐Ÿ“ค Upload the version change - id: upload - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: post-release-${{ github.run_id }}-${{ github.run_attempt }} - path: ${{ runner.temp }}/post-release/* - if-no-files-found: error - post-release: - name: ๐Ÿ”€ Open post-release version bump PR - needs: post-release-prepare - runs-on: ubuntu-26.04 - permissions: - contents: read - environment: release-auth - steps: - - name: ๐Ÿ”‘ Create release App token - id: token - uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 - with: - client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} - private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} - owner: ${{ github.repository_owner }} - repositories: ${{ github.event.repository.name }} - permission-contents: write - permission-pull-requests: write - permission-issues: write - - name: ๐Ÿ“ฉ Download the version change - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 - with: - artifact-ids: ${{ needs.post-release-prepare.outputs.artifact-id }} - path: ${{ runner.temp }}/post-release - - name: ๐Ÿ”€ Open the post-release PR - uses: tox-dev/github-workflows/publish-post-release-pr@57a765127aae73adf7d0e24a7fe0f067f2d3ea3e # 6.2.1 - env: - GH_TOKEN: ${{ steps.token.outputs.token }} - with: - base-commit: ${{ needs.post-release-prepare.outputs.base-commit }} - branch: ${{ needs.post-release-prepare.outputs.branch }} - title: Bump version to ${{ needs.post-release-prepare.outputs.next-version }} - body-file: ${{ runner.temp }}/post-release/body.md - paths-file: ${{ runner.temp }}/post-release/paths.tsv - allowed-paths: gradle.properties - bundle: ${{ needs.post-release-prepare.outputs.candidate-commit != '' && format('{0}/post-release/candidate.bundle', runner.temp) || '' }} - candidate-commit: ${{ needs.post-release-prepare.outputs.candidate-commit }} diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml index 21a4b48..a8ee2e2 100644 --- a/.github/workflows/prepare-release.yaml +++ b/.github/workflows/prepare-release.yaml @@ -15,7 +15,7 @@ concurrency: jobs: prepare: name: ๐Ÿท๏ธ Prepare Towncrier release - uses: tox-dev/github-workflows/.github/workflows/prepare-towncrier-release.yaml@57a765127aae73adf7d0e24a7fe0f067f2d3ea3e # 6.2.1 + uses: tox-dev/github-workflows/.github/workflows/prepare-towncrier-release.yaml@2c1f5e4ceda37c41eb8367b5cbeaa741aa60a566 # 6.5.2 permissions: contents: read pull-requests: read diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 78b5a5d..d493edf 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -8,128 +8,46 @@ concurrency: group: release-${{ github.ref }} cancel-in-progress: false jobs: - render-notes: - name: ๐Ÿ“ Render GitHub Release notes - runs-on: ubuntu-26.04 + build: + name: ๐Ÿงฉ JetBrains plugin check + uses: tox-dev/github-workflows/.github/workflows/jetbrains-plugin-check.yaml@2c1f5e4ceda37c41eb8367b5cbeaa741aa60a566 # 6.5.2 permissions: contents: read - outputs: - artifact-id: ${{ steps.upload.outputs.artifact-id }} - steps: - - name: ๐Ÿ“ฅ Check out code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - - name: ๐Ÿ Install uv - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 - with: - version: latest-known - python-version: "3.15" - enable-cache: false - - name: ๐Ÿ“ฆ Install locked release tools - uses: tox-dev/github-workflows/install-locked-tools@57a765127aae73adf7d0e24a7fe0f067f2d3ea3e # 6.2.1 - with: - groups: release-notes - venv: ${{ runner.temp }}/release-tools - - name: ๐Ÿ“ Render release notes - uses: tox-dev/github-workflows/render-release-notes@57a765127aae73adf7d0e24a7fe0f067f2d3ea3e # 6.2.1 - with: - tag: ${{ github.ref_name }} - changelog: CHANGELOG.md - artifact-url: https://plugins.jetbrains.com/plugin/20536-pyvenv-manage-2 - artifact-label: JetBrains Marketplace - - name: ๐Ÿ“ค Upload release notes - id: upload - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: release-notes - path: ${{ runner.temp }}/release-notes.md - if-no-files-found: error - build-unsigned: - name: ๐Ÿ—๏ธ Build unsigned plugin - runs-on: ubuntu-26.04 + with: + plugin-version: ${{ github.ref_name }} + release-notes: + name: ๐Ÿ“ Render GitHub Release notes + uses: tox-dev/github-workflows/.github/workflows/release-notes.yaml@2c1f5e4ceda37c41eb8367b5cbeaa741aa60a566 # 6.5.2 permissions: contents: read - outputs: - archive_sha: ${{ steps.digest.outputs.sha }} - artifact-id: ${{ steps.upload.outputs.artifact-id }} - steps: - - name: ๐Ÿ“ฅ Check out code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - name: ๐Ÿงฝ Free disk space - uses: jlumbroso/free-disk-space@ceedf095f4ec1a097402bc6bd80831f2e1a6fde6 # v2.0.0 - with: - tool-cache: false - large-packages: false - - name: ๐Ÿงฐ Set up mise (JDKs) - uses: jdx/mise-action@6be3c7ed4c21d4f76b208d351d95aac273ebb76d # v5.2.4 - with: - cache: false - - name: โ˜• Set up Gradle - uses: gradle/actions/setup-gradle@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 # zizmor: ignore[cache-poisoning] - with: - cache-disabled: true - - name: ๐Ÿท๏ธ Set version from tag - env: - TAG: ${{ github.ref_name }} - run: sed -i "s/^pluginVersion=.*/pluginVersion=${TAG#v}/" gradle.properties - - name: ๐Ÿ—๏ธ Build plugin ZIP - run: ./gradlew buildPlugin - - name: โš™๏ธ Select unsigned ZIP - id: archive - env: - TAG: ${{ github.ref_name }} - run: | - archives=(build/distributions/*-"${TAG#v}".zip) - echo "path=${archives[0]}" >>"$GITHUB_OUTPUT" - - name: โš™๏ธ Record unsigned ZIP digest - id: digest - env: - ARCHIVE: ${{ steps.archive.outputs.path }} - run: | - read -r digest _ < <(sha256sum "$ARCHIVE") - echo "sha=$digest" >>"$GITHUB_OUTPUT" - - name: ๐Ÿ“ค Upload unsigned plugin - id: upload - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: unsigned-plugin - path: ${{ steps.archive.outputs.path }} - if-no-files-found: error + with: + tag: ${{ github.ref_name }} + artifact-url: https://plugins.jetbrains.com/plugin/20536-pyvenv-manage-2 + artifact-label: JetBrains Marketplace sign: name: โœ๏ธ Sign plugin ZIP - needs: build-unsigned + needs: build runs-on: ubuntu-26.04 permissions: {} environment: name: release url: https://plugins.jetbrains.com/plugin/20536-pyvenv-manage-2 outputs: - archive_sha: ${{ steps.signed.outputs.sha256 }} - archive_name: ${{ steps.archive.outputs.name }} + archive-name: ${{ steps.archive.outputs.name }} + sha256: ${{ steps.signed.outputs.sha256 }} artifact-id: ${{ steps.upload.outputs.artifact-id }} steps: - name: ๐Ÿ“ฉ Download unsigned plugin uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: - artifact-ids: ${{ needs.build-unsigned.outputs.artifact-id }} - path: dist/ - - name: โš™๏ธ Select unsigned ZIP - id: unsigned - env: - TAG: ${{ github.ref_name }} - run: | - archives=(dist/*-"${TAG#v}".zip) - echo "path=${archives[0]}" >>"$GITHUB_OUTPUT" + artifact-ids: ${{ needs.build.outputs.artifact-id }} + path: dist - name: โœ๏ธ Sign verified plugin ZIP id: signed - uses: tox-dev/github-workflows/sign-jetbrains-plugin@57a765127aae73adf7d0e24a7fe0f067f2d3ea3e # 6.2.1 + uses: tox-dev/github-workflows/sign-jetbrains-plugin@2c1f5e4ceda37c41eb8367b5cbeaa741aa60a566 # 6.5.2 with: - archive: ${{ steps.unsigned.outputs.path }} - sha256: ${{ needs.build-unsigned.outputs.archive_sha }} + archive: dist/${{ needs.build.outputs.archive-name }} + sha256: ${{ needs.build.outputs.archive-sha256 }} certificate-chain: ${{ secrets.CERTIFICATE_CHAIN }} private-key: ${{ secrets.PRIVATE_KEY }} private-key-password: ${{ secrets.PRIVATE_KEY_PASSWORD }} @@ -145,6 +63,7 @@ jobs: name: plugin-distribution path: ${{ steps.signed.outputs.archive }} if-no-files-found: error + retention-days: 30 marketplace-check: name: ๐Ÿ” Check Marketplace version needs: sign @@ -152,124 +71,176 @@ jobs: permissions: contents: read outputs: - publish: ${{ steps.marketplace.outputs.publish }} + publish: ${{ steps.stage.outputs.publish }} + channel: ${{ steps.stage.outputs.channel }} steps: - name: ๐Ÿ“ฅ Check out code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + - name: ๐Ÿ“ฉ Download signed plugin + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + artifact-ids: ${{ needs.sign.outputs.artifact-id }} + path: dist - name: ๐Ÿ Install uv - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: version: latest-known python-version: "3.15" enable-cache: false - name: ๐Ÿ“ฆ Install locked release tools - uses: tox-dev/github-workflows/install-locked-tools@57a765127aae73adf7d0e24a7fe0f067f2d3ea3e # 6.2.1 + uses: tox-dev/github-workflows/install-locked-tools@2c1f5e4ceda37c41eb8367b5cbeaa741aa60a566 # 6.5.2 with: groups: release-notes venv: ${{ runner.temp }}/release-tools - - name: ๐Ÿ“ฉ Download signed plugin - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 - with: - artifact-ids: ${{ needs.sign.outputs.artifact-id }} - path: dist/ - - name: โš™๏ธ Select signed ZIP - id: archive - run: | - archives=(dist/*-signed.zip) - echo "path=${archives[0]}" >>"$GITHUB_OUTPUT" - - name: ๐Ÿ” Verify signed ZIP artifact - env: - EXPECTED_SHA: ${{ needs.sign.outputs.archive_sha }} - ARCHIVE: ${{ steps.archive.outputs.path }} - run: | - echo "$EXPECTED_SHA $ARCHIVE" | sha256sum --check - name: ๐Ÿ” Compare Marketplace version - id: marketplace - env: - TAG: ${{ github.ref_name }} - ARCHIVE: ${{ steps.archive.outputs.path }} - run: | - release-creator marketplace --plugin-id com.github.pyvenvmanage.pyvenv \ - --version "${TAG#v}" --archive "$ARCHIVE" --mode stage --github-output "$GITHUB_OUTPUT" + id: stage + uses: tox-dev/github-workflows/stage-jetbrains-marketplace@2c1f5e4ceda37c41eb8367b5cbeaa741aa60a566 # 6.5.2 + with: + archive: dist/${{ needs.sign.outputs.archive-name }} + sha256: ${{ needs.sign.outputs.sha256 }} + plugin-id: com.github.pyvenvmanage.pyvenv + version: ${{ github.ref_name }} + mode: stage marketplace-publish: name: ๐Ÿš€ Publish to JetBrains Marketplace - needs: [sign, render-notes, marketplace-check] + needs: [sign, release-notes, marketplace-check] if: needs.marketplace-check.outputs.publish == 'true' runs-on: ubuntu-26.04 permissions: actions: read - environment: - name: release - url: https://plugins.jetbrains.com/plugin/20536-pyvenv-manage-2 + contents: read + environment: release steps: - name: ๐Ÿ“ฉ Download signed plugin uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: artifact-ids: ${{ needs.sign.outputs.artifact-id }} - path: dist/ + path: dist - name: ๐Ÿ“ฉ Download release notes uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: - artifact-ids: ${{ needs.render-notes.outputs.artifact-id }} + artifact-ids: ${{ needs.release-notes.outputs.artifact-id }} path: ${{ runner.temp }}/marketplace-notes - - name: โš™๏ธ Select release channel - id: channel - env: - TAG: ${{ github.ref_name }} - run: | - version="${TAG#v}" - channel="${version#*-}" - if [[ "$channel" == "$version" ]]; then channel=''; fi - echo "name=${channel%%.*}" >>"$GITHUB_OUTPUT" - name: ๐Ÿš€ Publish signed ZIP - uses: tox-dev/github-workflows/publish-jetbrains-marketplace@57a765127aae73adf7d0e24a7fe0f067f2d3ea3e # 6.2.1 + uses: tox-dev/github-workflows/publish-jetbrains-marketplace@2c1f5e4ceda37c41eb8367b5cbeaa741aa60a566 # 6.5.2 with: - archive: dist/${{ needs.sign.outputs.archive_name }} - sha256: ${{ needs.sign.outputs.archive_sha }} + archive: dist/${{ needs.sign.outputs.archive-name }} + sha256: ${{ needs.sign.outputs.sha256 }} plugin-id: "20536" - channel: ${{ steps.channel.outputs.name }} + channel: ${{ needs.marketplace-check.outputs.channel }} tag: ${{ github.ref_name }} commit: ${{ github.sha }} signed-artifact-id: ${{ needs.sign.outputs.artifact-id }} - notes-artifact-id: ${{ needs.render-notes.outputs.artifact-id }} + notes-artifact-id: ${{ needs.release-notes.outputs.artifact-id }} notes-file: ${{ runner.temp }}/marketplace-notes/release-notes.md github-token: ${{ github.token }} publish-token: ${{ secrets.PUBLISH_TOKEN }} github-release: name: ๐Ÿš€ Publish GitHub Release - needs: [sign, render-notes, marketplace-check, marketplace-publish] - if: >- - always() && needs.sign.result == 'success' && needs.render-notes.result == 'success' && needs.marketplace-check.result == 'success' && (needs.marketplace-publish.result == 'success' || (needs.marketplace-publish.result == 'skipped' && needs.marketplace-check.outputs.publish == 'false')) + needs: [sign, release-notes, marketplace-check, marketplace-publish] + if: always() && needs.release-notes.result == 'success' && needs.sign.result == 'success' && needs.marketplace-check.result == 'success' && (needs.marketplace-publish.result == 'success' || (needs.marketplace-publish.result == 'skipped' && needs.marketplace-check.outputs.publish == 'false')) runs-on: ubuntu-26.04 permissions: - contents: write + contents: read + environment: release steps: - name: ๐Ÿ“ฉ Download release notes uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: - artifact-ids: ${{ needs.render-notes.outputs.artifact-id }} - path: ${{ runner.temp }}/release-notes + artifact-ids: ${{ needs.release-notes.outputs.artifact-id }} + path: ${{ runner.temp }} - name: ๐Ÿ“ฉ Download signed plugin uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: artifact-ids: ${{ needs.sign.outputs.artifact-id }} path: dist/ - - name: ๐Ÿ” Verify downloaded plugin ZIP digest + - name: ๐Ÿ” Check downloaded plugin ZIP env: - EXPECTED_SHA: ${{ needs.sign.outputs.archive_sha }} + EXPECTED_SHA: ${{ needs.sign.outputs.sha256 }} run: | archives=(dist/*-signed.zip) read -r digest _ < <(sha256sum "${archives[0]}") test "$digest" = "$EXPECTED_SHA" - name: ๐Ÿš€ Publish GitHub release - uses: tox-dev/github-workflows/publish-github-release@57a765127aae73adf7d0e24a7fe0f067f2d3ea3e # 6.2.1 + uses: tox-dev/github-workflows/publish-github-release@2c1f5e4ceda37c41eb8367b5cbeaa741aa60a566 # 6.5.2 env: GH_TOKEN: ${{ github.token }} with: tag: ${{ github.ref_name }} commit: ${{ github.sha }} title: ${{ github.ref_name }} - notes-file: ${{ runner.temp }}/release-notes/release-notes.md + notes-file: ${{ runner.temp }}/release-notes.md assets-directory: dist + post-release-prepare: + name: ๐Ÿท๏ธ Prepare post-release version bump + needs: github-release + runs-on: ubuntu-26.04 + permissions: + contents: read + outputs: + base-commit: ${{ steps.change.outputs.base-commit }} + candidate-commit: ${{ steps.change.outputs.candidate-commit }} + branch: ${{ steps.change.outputs.branch }} + next-version: ${{ steps.change.outputs.next-version }} + artifact-id: ${{ steps.upload.outputs.artifact-id }} + steps: + - name: ๐Ÿ“ฅ Check out code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: main + fetch-depth: 0 + persist-credentials: false + - name: ๐Ÿท๏ธ Prepare the exact version change + id: change + uses: tox-dev/github-workflows/prepare-jetbrains-post-release@2c1f5e4ceda37c41eb8367b5cbeaa741aa60a566 # 6.5.2 + with: + tag: ${{ github.ref_name }} + app-slug: tox-dev-pyvenvmanage-release + github-token: ${{ github.token }} + output-dir: ${{ runner.temp }}/post-release + - name: ๐Ÿ“ค Upload the version change + id: upload + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: post-release-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/post-release/* + if-no-files-found: error + post-release: + name: ๐Ÿ”€ Open post-release version bump PR + needs: post-release-prepare + runs-on: ubuntu-26.04 + permissions: + contents: read + environment: release-auth + steps: + - name: ๐Ÿ”‘ Create release App token + id: token + uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 + with: + client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: ${{ github.event.repository.name }} + permission-contents: write + permission-pull-requests: write + permission-issues: write + - name: ๐Ÿ“ฉ Download the version change + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + artifact-ids: ${{ needs.post-release-prepare.outputs.artifact-id }} + path: ${{ runner.temp }}/post-release + - name: ๐Ÿ”€ Open the post-release PR + uses: tox-dev/github-workflows/publish-post-release-pr@2c1f5e4ceda37c41eb8367b5cbeaa741aa60a566 # 6.5.2 + env: + GH_TOKEN: ${{ steps.token.outputs.token }} + with: + base-commit: ${{ needs.post-release-prepare.outputs.base-commit }} + branch: ${{ needs.post-release-prepare.outputs.branch }} + title: Bump version to ${{ needs.post-release-prepare.outputs.next-version }} + body-file: ${{ runner.temp }}/post-release/body.md + paths-file: ${{ runner.temp }}/post-release/paths.tsv + allowed-paths: gradle.properties + bundle: ${{ needs.post-release-prepare.outputs.candidate-commit != '' && format('{0}/post-release/candidate.bundle', runner.temp) || '' }} + candidate-commit: ${{ needs.post-release-prepare.outputs.candidate-commit }} diff --git a/.github/workflows/scorecard.yaml b/.github/workflows/scorecard.yaml index b092701..9c85c5f 100644 --- a/.github/workflows/scorecard.yaml +++ b/.github/workflows/scorecard.yaml @@ -23,4 +23,4 @@ jobs: pull-requests: read # read reviews for the Code-Review check security-events: write # upload the SARIF results to code scanning statuses: read # read commit statuses for the CI-Tests check - uses: tox-dev/github-workflows/.github/workflows/scorecard.yaml@cfb6d637a10b70da576575790eb3c806f57cd0b5 + uses: tox-dev/github-workflows/.github/workflows/scorecard.yaml@2c1f5e4ceda37c41eb8367b5cbeaa741aa60a566 # 6.5.2 diff --git a/.github/workflows/upgrade-release-tools.yaml b/.github/workflows/upgrade-release-tools.yaml index f2e3aa1..12bd92c 100644 --- a/.github/workflows/upgrade-release-tools.yaml +++ b/.github/workflows/upgrade-release-tools.yaml @@ -10,7 +10,7 @@ concurrency: jobs: upgrade: name: ๐Ÿ”€ Upgrade release tools - uses: tox-dev/github-workflows/.github/workflows/upgrade-release-tools.yaml@57a765127aae73adf7d0e24a7fe0f067f2d3ea3e # 6.2.1 + uses: tox-dev/github-workflows/.github/workflows/upgrade-release-tools.yaml@2c1f5e4ceda37c41eb8367b5cbeaa741aa60a566 # 6.5.2 permissions: contents: read secrets: diff --git a/.github/workflows/verify-marketplace-release.yaml b/.github/workflows/verify-marketplace-release.yaml index 2e5a113..2152ae7 100644 --- a/.github/workflows/verify-marketplace-release.yaml +++ b/.github/workflows/verify-marketplace-release.yaml @@ -19,13 +19,13 @@ jobs: ref: main persist-credentials: false - name: ๐Ÿ Install uv - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2 + uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: version: latest-known python-version: "3.15" enable-cache: false - name: ๐Ÿ“ฆ Install locked release tools - uses: tox-dev/github-workflows/install-locked-tools@57a765127aae73adf7d0e24a7fe0f067f2d3ea3e # 6.2.1 + uses: tox-dev/github-workflows/install-locked-tools@2c1f5e4ceda37c41eb8367b5cbeaa741aa60a566 # 6.5.2 with: groups: release-notes venv: ${{ runner.temp }}/release-tools @@ -41,6 +41,7 @@ jobs: archives=(dist/*-signed.zip) echo "path=${archives[0]}" >>"$GITHUB_OUTPUT" - name: ๐Ÿ” Check GitHub asset digest + id: digest env: GH_TOKEN: ${{ github.token }} TAG: ${{ inputs.tag }} @@ -52,8 +53,12 @@ jobs: expected=$(jq -r --arg name "$name" '[.assets[] | select(.name == $name) | .digest] | if length == 1 then .[0] else error("signed asset set differs") end' "$RUNNER_TEMP/release.json") test "$expected" = "sha256:$(sha256sum "$ARCHIVE" | cut -d' ' -f1)" + echo "sha256=${expected#sha256:}" >>"$GITHUB_OUTPUT" - name: ๐Ÿ” Compare public Marketplace member bytes - env: - TAG: ${{ inputs.tag }} - ARCHIVE: ${{ steps.archive.outputs.path }} - run: release-creator marketplace --plugin-id com.github.pyvenvmanage.pyvenv --version "${TAG#v}" --archive "$ARCHIVE" --mode verify + uses: tox-dev/github-workflows/stage-jetbrains-marketplace@2c1f5e4ceda37c41eb8367b5cbeaa741aa60a566 # 6.5.2 + with: + archive: ${{ steps.archive.outputs.path }} + sha256: ${{ steps.digest.outputs.sha256 }} + plugin-id: com.github.pyvenvmanage.pyvenv + version: ${{ inputs.tag }} + mode: verify