Release #15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: "Release version X.Y.Z (required for release and dry-run)" | |
| required: false | |
| type: string | |
| mode: | |
| description: "What to run" | |
| type: choice | |
| default: dry-run | |
| options: | |
| - dry-run | |
| - release | |
| - manifest-only | |
| - transparency-only | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: grove-release-${{ inputs.mode }} | |
| cancel-in-progress: false | |
| jobs: | |
| validate: | |
| if: inputs.mode == 'release' || inputs.mode == 'dry-run' | |
| runs-on: ubuntu-latest | |
| env: | |
| VERSION: ${{ inputs.version }} | |
| steps: | |
| - name: Validate inputs | |
| run: | | |
| set -euo pipefail | |
| echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$' || { | |
| echo "version must match X.Y.Z, got: $VERSION" | |
| exit 1 | |
| } | |
| build: | |
| if: inputs.mode == 'release' || inputs.mode == 'dry-run' | |
| needs: validate | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - { os: ubuntu-latest, target: linux-x64, ext: "" } | |
| - { os: macos-latest, target: macos-arm64, ext: "" } | |
| - { os: macos-15-intel, target: macos-x64, ext: "" } | |
| - { os: windows-latest, target: windows-x64, ext: ".exe" } | |
| runs-on: ${{ matrix.os }} | |
| env: | |
| VERSION: ${{ inputs.version }} | |
| MODE: ${{ inputs.mode }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ inputs.mode == 'release' && format('refs/tags/v{0}', inputs.version) || '' }} | |
| - name: Assert checkout matches the tag | |
| if: inputs.mode == 'release' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| tag_commit=$(git ls-remote origin "refs/tags/v$VERSION" "refs/tags/v$VERSION^{}" | tail -1 | cut -f1) | |
| [ -n "$tag_commit" ] || { echo "tag v$VERSION not found on origin"; exit 1; } | |
| [ "$(git rev-parse HEAD)" = "$tag_commit" ] || { echo "checkout does not match tag v$VERSION"; exit 1; } | |
| - name: Build grove and grove-mcp | |
| run: cargo build --release --locked -p grove-core -p grove-mcp | |
| shell: bash | |
| - name: Test | |
| run: cargo test --workspace --exclude grove-desktop --locked | |
| shell: bash | |
| - name: Package archives | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p stage out | |
| for comp in grove grove-mcp; do | |
| cp "target/release/$comp${{ matrix.ext }}" stage/ | |
| tar -czf "out/$comp-v$VERSION-${{ matrix.target }}.tar.gz" -C stage "$comp${{ matrix.ext }}" | |
| rm "stage/$comp${{ matrix.ext }}" | |
| done | |
| ls -la out/ | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: dist-${{ matrix.target }} | |
| path: out/*.tar.gz | |
| build-desktop: | |
| if: inputs.mode == 'release' || inputs.mode == 'dry-run' | |
| needs: validate | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - { os: ubuntu-latest, target: linux-x64, ext: "", bundles: "deb,appimage" } | |
| - { os: macos-latest, target: macos-arm64, ext: "", bundles: "dmg" } | |
| - { os: macos-15-intel, target: macos-x64, ext: "", bundles: "dmg" } | |
| - { os: windows-latest, target: windows-x64, ext: ".exe", bundles: "nsis,msi" } | |
| runs-on: ${{ matrix.os }} | |
| env: | |
| VERSION: ${{ inputs.version }} | |
| MODE: ${{ inputs.mode }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ inputs.mode == 'release' && format('refs/tags/v{0}', inputs.version) || '' }} | |
| - name: Assert checkout matches the tag | |
| if: inputs.mode == 'release' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| tag_commit=$(git ls-remote origin "refs/tags/v$VERSION" "refs/tags/v$VERSION^{}" | tail -1 | cut -f1) | |
| [ -n "$tag_commit" ] || { echo "tag v$VERSION not found on origin"; exit 1; } | |
| [ "$(git rev-parse HEAD)" = "$tag_commit" ] || { echo "checkout does not match tag v$VERSION"; exit 1; } | |
| - name: Install Tauri system dependencies | |
| if: runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libwebkit2gtk-4.1-dev libxdo-dev libappindicator3-dev librsvg2-dev | |
| - name: Install tauri-cli 2.11.4 | |
| run: cargo install tauri-cli --locked --version 2.11.4 | |
| shell: bash | |
| - name: Build desktop bundles | |
| run: cargo tauri build --bundles "${{ matrix.bundles }}" | |
| shell: bash | |
| working-directory: packages/desktop/src-tauri | |
| - name: Desktop test suite | |
| run: cargo test -p grove-desktop --locked | |
| shell: bash | |
| - name: Package portable archive and bundles | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p stage/ui out | |
| cp "target/release/grove-desktop${{ matrix.ext }}" stage/ | |
| cp packages/desktop/src-tauri/icons/128x128.png stage/icon.png | |
| cp packages/desktop/src-tauri/icons/icon.icns stage/icon.icns | |
| cp -r packages/desktop/ui/views stage/ui/views | |
| cp -r packages/desktop/ui/icons stage/ui/icons | |
| tar -czf "out/grove-desktop-v$VERSION-${{ matrix.target }}.tar.gz" -C stage "grove-desktop${{ matrix.ext }}" icon.png icon.icns ui | |
| cd target/release/bundle | |
| case "${{ matrix.target }}" in | |
| linux-x64) | |
| cp deb/*.deb "$OLDPWD/out/grove-desktop-v$VERSION-linux-x64.deb" | |
| cp appimage/*.AppImage "$OLDPWD/out/grove-desktop-v$VERSION-linux-x64.AppImage" | |
| ;; | |
| macos-*) | |
| cp dmg/*.dmg "$OLDPWD/out/grove-desktop-v$VERSION-${{ matrix.target }}.dmg" | |
| ;; | |
| windows-x64) | |
| cp nsis/*-setup.exe "$OLDPWD/out/grove-desktop-v$VERSION-windows-x64-setup.exe" | |
| cp msi/*.msi "$OLDPWD/out/grove-desktop-v$VERSION-windows-x64.msi" | |
| ;; | |
| esac | |
| ls -la "$OLDPWD/out/" | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: dist-desktop-${{ matrix.target }} | |
| path: out/* | |
| audit: | |
| if: inputs.mode == 'release' || inputs.mode == 'dry-run' | |
| needs: validate | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - name: Install trivy 0.73.0 | |
| run: | | |
| set -euo pipefail | |
| curl -fsSL -o /tmp/trivy.tar.gz https://github.com/aquasecurity/trivy/releases/download/v0.73.0/trivy_0.73.0_Linux-64bit.tar.gz | |
| curl -fsSL -o /tmp/trivy-checksums.txt https://github.com/aquasecurity/trivy/releases/download/v0.73.0/trivy_0.73.0_checksums.txt | |
| expected=$(grep "Linux-64bit.tar.gz$" /tmp/trivy-checksums.txt | cut -d' ' -f1) | |
| actual=$(sha256sum /tmp/trivy.tar.gz | cut -d' ' -f1) | |
| [ "$actual" = "$expected" ] || { echo "trivy checksum mismatch"; exit 1; } | |
| sudo tar -xzf /tmp/trivy.tar.gz -C /usr/local/bin trivy | |
| - uses: julia-actions/setup-julia@fa02766e078afaaf09b14210362cee14137e6a32 # v3.0.2 | |
| with: | |
| version: "1.12" | |
| - name: Instantiate Julia project | |
| run: julia --project=packages/grove -e 'using Pkg; Pkg.instantiate()' | |
| - name: Audit gate (fail-closed) | |
| run: bin/audit.sh --trivy-bin trivy --fail-closed | |
| sbom: | |
| if: inputs.mode == 'release' || inputs.mode == 'dry-run' | |
| needs: validate | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: julia-actions/setup-julia@fa02766e078afaaf09b14210362cee14137e6a32 # v3.0.2 | |
| with: | |
| version: "1.12" | |
| - name: Instantiate Julia project | |
| run: julia --project=packages/grove -e 'using Pkg; Pkg.instantiate()' | |
| - name: Install trivy 0.73.0 | |
| run: | | |
| set -euo pipefail | |
| curl -fsSL -o /tmp/trivy.tar.gz https://github.com/aquasecurity/trivy/releases/download/v0.73.0/trivy_0.73.0_Linux-64bit.tar.gz | |
| curl -fsSL -o /tmp/trivy-checksums.txt https://github.com/aquasecurity/trivy/releases/download/v0.73.0/trivy_0.73.0_checksums.txt | |
| expected=$(grep "Linux-64bit.tar.gz$" /tmp/trivy-checksums.txt | cut -d' ' -f1) | |
| actual=$(sha256sum /tmp/trivy.tar.gz | cut -d' ' -f1) | |
| [ "$actual" = "$expected" ] || { echo "trivy checksum mismatch"; exit 1; } | |
| sudo tar -xzf /tmp/trivy.tar.gz -C /usr/local/bin trivy | |
| - name: Install cargo-cyclonedx 0.5.9 | |
| run: cargo install cargo-cyclonedx --locked --version 0.5.9 | |
| - name: Generate SBOM | |
| run: bin/sbom.sh --output sbom.cdx.json | |
| - name: Validate VEX | |
| run: julia --project=packages/grove bin/validate-vex.jl docs/security/artifacts/vex.json | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: sbom | |
| path: sbom.cdx.json | |
| sign-and-publish: | |
| needs: [build, build-desktop, audit, sbom] | |
| if: always() && !failure() && !cancelled() | |
| runs-on: ubuntu-latest | |
| environment: ${{ inputs.mode == 'dry-run' && 'release-dry' || 'release' }} | |
| permissions: | |
| contents: write | |
| id-token: write | |
| attestations: write | |
| pull-requests: write | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ inputs.version }} | |
| MODE: ${{ inputs.mode }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| ref: ${{ inputs.mode == 'release' && format('refs/tags/v{0}', inputs.version) || '' }} | |
| - name: Download build artifacts | |
| if: inputs.mode == 'release' || inputs.mode == 'dry-run' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: dist-* | |
| path: out | |
| merge-multiple: true | |
| - name: Download SBOM | |
| if: inputs.mode == 'release' || inputs.mode == 'dry-run' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: sbom | |
| path: sbom | |
| - name: Prepare signing key | |
| run: | | |
| set -euo pipefail | |
| umask 077 | |
| if [ "$MODE" = "dry-run" ]; then | |
| openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out key.pem 2>/dev/null | |
| openssl pkey -in key.pem -pubout -out trusted.pem 2>/dev/null | |
| echo "dry-run: using an ephemeral in-job key; the release secret is not touched" | |
| else | |
| printf '%s' "$GROVE_MANIFEST_SIGNING_KEY" > key.pem | |
| cp docs/security/artifacts/public-keys/grove-manifest-2026-08.pem trusted.pem | |
| chmod 600 key.pem | |
| openssl rsa -in key.pem -pubout -outform DER 2>/dev/null | sha256sum | cut -c1-16 | |
| openssl pkey -pubin -in trusted.pem -outform DER 2>/dev/null | sha256sum | cut -c1-16 | |
| fi | |
| env: | |
| GROVE_MANIFEST_SIGNING_KEY: ${{ secrets.GROVE_MANIFEST_SIGNING_KEY }} | |
| - name: Check release preconditions | |
| if: inputs.mode == 'release' | |
| run: | | |
| set -euo pipefail | |
| echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$' || { echo "bad version"; exit 1; } | |
| gh api "repos/$GITHUB_REPOSITORY/git/refs/tags/v$VERSION" > /dev/null || { | |
| echo "tag v$VERSION does not exist; create and push it before dispatching (see publish-release runbook)" | |
| exit 1 | |
| } | |
| if gh api "repos/$GITHUB_REPOSITORY/releases/tags/v$VERSION" > /dev/null 2>&1; then | |
| echo "release v$VERSION already exists" | |
| exit 1 | |
| fi | |
| - name: Sign release metadata | |
| if: inputs.mode == 'release' || inputs.mode == 'dry-run' | |
| run: | | |
| set -euo pipefail | |
| ls out/*.tar.gz | |
| (cd out && ls | grep -v '^SHA256SUMS' | xargs sha256sum > SHA256SUMS) | |
| bin/sign.sh key.pem out/SHA256SUMS | |
| bin/manifest.sh --version "$VERSION" --sums out/SHA256SUMS --artifacts-dir out \ | |
| --previous manifest.json --output manifest.json | |
| bin/sign.sh key.pem manifest.json | |
| cp sbom/sbom.cdx.json docs/security/artifacts/sbom.cdx.json | |
| bin/sign.sh key.pem docs/security/artifacts/sbom.cdx.json | |
| bin/sign.sh key.pem docs/security/artifacts/vex.json | |
| bin/skill-bundle.sh --output out/grove-skill.md | |
| bin/sign.sh key.pem out/grove-skill.md | |
| if ! bin/verify.sh trusted.pem install.sh install.sh.sig 2>/dev/null; then | |
| bin/sign.sh key.pem install.sh | |
| fi | |
| if ! bin/verify.sh trusted.pem install.ps1 install.ps1.sig 2>/dev/null; then | |
| bin/sign.sh key.pem install.ps1 | |
| fi | |
| bin/verify.sh trusted.pem manifest.json manifest.json.sig | |
| cat > out/notes.md <<EOF | |
| ## grove v$VERSION | |
| ### Install | |
| bash (macOS / Linux / Git Bash on Windows): | |
| curl -fsSL https://raw.githubusercontent.com/alxshelepenok/grove/main/install.sh | bash | |
| Windows PowerShell 5.1+: | |
| iwr https://raw.githubusercontent.com/alxshelepenok/grove/main/install.ps1 -UseBasicParsing | iex | |
| The installer verifies the signed manifest (RSA-2048/PSS) before parsing it and checks SHA-256 + size of every artifact before installing. Anti-rollback state lives in ~/.grove/.sequence. | |
| ### Verify manually | |
| Download SHA256SUMS and SHA256SUMS.sig from this release, then: | |
| bin/verify.sh docs/security/artifacts/public-keys/grove-manifest-2026-08.pem SHA256SUMS SHA256SUMS.sig | |
| sha256sum -c SHA256SUMS | |
| ### Desktop bundles are unsigned | |
| Browser downloads of the .msi / .dmg / .deb / .AppImage carry no publisher signature: Windows SmartScreen and macOS Gatekeeper will warn. The signed manifest and SHA256SUMS.sig above are the verification path; the script installers never trigger these prompts (command-line downloads carry no Mark-of-the-Web / quarantine attribute). | |
| ### Contents | |
| - grove and grove-mcp CLI binaries + grove-desktop portable archives: linux-x64, macos-arm64, macos-x64, windows-x64. | |
| - Desktop OS bundles: msi + nsis (windows), dmg (macOS arm64 + x64), deb + AppImage (linux). | |
| - Signed manifest, SHA256SUMS, CycloneDX SBOM, VEX, signed installers. | |
| EOF | |
| cat manifest.json | |
| - name: Dry-run summary | |
| if: inputs.mode == 'dry-run' | |
| run: | | |
| set -euo pipefail | |
| echo "dry-run complete; nothing was published" | |
| echo "would attach to release v$VERSION:" | |
| ls out/*.tar.gz out/SHA256SUMS* manifest.json* docs/security/artifacts/sbom.cdx.json* docs/security/artifacts/vex.json.sig | |
| - name: Refresh manifest only | |
| if: inputs.mode == 'manifest-only' | |
| run: | | |
| set -euo pipefail | |
| bin/manifest.sh --refresh --previous manifest.json --output manifest.json | |
| bin/sign.sh key.pem manifest.json | |
| bin/verify.sh trusted.pem manifest.json manifest.json.sig | |
| tag=$(gh release list --limit 1 --json tagName --jq '.[0].tagName') | |
| [ -n "$tag" ] || { echo "no release to attach the refreshed manifest to"; exit 1; } | |
| gh release upload "$tag" manifest.json manifest.json.sig --clobber | |
| - name: Re-sign transparency artifacts only | |
| if: inputs.mode == 'transparency-only' | |
| run: | | |
| set -euo pipefail | |
| curl -fsSL -o /tmp/julia.tar.gz https://julialang-s3.julialang.org/bin/linux/x64/1.12/julia-1.12.6-linux-x86_64.tar.gz | |
| echo "bbabf3bef19421a9dbd24a767d807606ab85e444323b5a1c73ffe293fa3d079a /tmp/julia.tar.gz" | sha256sum -c - | |
| sudo tar -xzf /tmp/julia.tar.gz -C /opt | |
| export PATH="/opt/julia-1.12.6/bin:$PATH" | |
| julia --project=packages/grove -e 'using Pkg; Pkg.instantiate()' | |
| julia --project=packages/grove bin/validate-vex.jl docs/security/artifacts/vex.json | |
| bin/sign.sh key.pem docs/security/artifacts/vex.json | |
| [ -f docs/security/artifacts/sbom.cdx.json ] && bin/sign.sh key.pem docs/security/artifacts/sbom.cdx.json | |
| tag=$(gh release list --limit 1 --json tagName --jq '.[0].tagName') | |
| [ -n "$tag" ] || { echo "no release to attach transparency artifacts to"; exit 1; } | |
| gh release upload "$tag" docs/security/artifacts/vex.json docs/security/artifacts/vex.json.sig --clobber | |
| [ -f docs/security/artifacts/sbom.cdx.json ] && gh release upload "$tag" docs/security/artifacts/sbom.cdx.json docs/security/artifacts/sbom.cdx.json.sig --clobber | |
| - name: Publish release | |
| if: inputs.mode == 'release' | |
| run: | | |
| set -euo pipefail | |
| gh release create "v$VERSION" \ | |
| --title "v$VERSION" \ | |
| --notes-file out/notes.md \ | |
| --verify-tag \ | |
| out/*.tar.gz out/*.deb out/*.AppImage out/*.dmg out/*.msi out/*-setup.exe \ | |
| out/SHA256SUMS out/SHA256SUMS.sig \ | |
| out/grove-skill.md out/grove-skill.md.sig \ | |
| manifest.json manifest.json.sig \ | |
| docs/security/artifacts/sbom.cdx.json docs/security/artifacts/sbom.cdx.json.sig \ | |
| docs/security/artifacts/vex.json docs/security/artifacts/vex.json.sig \ | |
| install.sh install.sh.sig install.ps1 install.ps1.sig | |
| - name: Attest build provenance | |
| if: inputs.mode == 'release' | |
| uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 | |
| with: | |
| subject-path: | | |
| out/*.tar.gz | |
| out/*.deb | |
| out/*.AppImage | |
| out/*.dmg | |
| out/*.msi | |
| out/*-setup.exe | |
| manifest.json | |
| - name: Post-release smoke test | |
| if: inputs.mode == 'release' | |
| run: | | |
| set -euo pipefail | |
| base="https://github.com/$GITHUB_REPOSITORY/releases/download/v$VERSION" | |
| mkdir -p smoke | |
| curl -fsSL -o smoke/manifest.json "$base/manifest.json" | |
| curl -fsSL -o smoke/manifest.json.sig "$base/manifest.json.sig" | |
| bin/verify.sh trusted.pem smoke/manifest.json smoke/manifest.json.sig | |
| for f in install.sh install.ps1; do | |
| curl -fsSL -o "smoke/$f" "$base/$f" | |
| curl -fsSL -o "smoke/$f.sig" "$base/$f.sig" | |
| bin/verify.sh trusted.pem "smoke/$f" "smoke/$f.sig" | |
| done | |
| expect=$(sed -n '/^ "grove_mcp_linux_x64": {$/,/^ }/p' smoke/manifest.json | sed -n 's/^ "sha256": "\([0-9a-f]*\)",$/\1/p') | |
| curl -fsSL -o smoke/grove-mcp.tar.gz "$base/grove-mcp-v$VERSION-linux-x64.tar.gz" | |
| actual=$(sha256sum smoke/grove-mcp.tar.gz | cut -d' ' -f1) | |
| [ "$actual" = "$expect" ] || { echo "smoke hash mismatch"; exit 1; } | |
| expect_dt=$(sed -n '/^ "grove_desktop_linux_x64": {$/,/^ }/p' smoke/manifest.json | sed -n 's/^ "sha256": "\([0-9a-f]*\)",$/\1/p') | |
| curl -fsSL -o smoke/grove-desktop.tar.gz "$base/grove-desktop-v$VERSION-linux-x64.tar.gz" | |
| actual_dt=$(sha256sum smoke/grove-desktop.tar.gz | cut -d' ' -f1) | |
| [ "$actual_dt" = "$expect_dt" ] || { echo "smoke desktop hash mismatch"; exit 1; } | |
| echo "smoke ok: published manifest, install.sh, install.ps1 verify; grove-mcp and grove-desktop linux-x64 match their signed hashes" | |
| - name: Commit transparency log | |
| if: inputs.mode != 'dry-run' | |
| run: | | |
| set -euo pipefail | |
| git config user.name "grove-compass[bot]" | |
| git config user.email "324460858+grove-compass[bot]@users.noreply.github.com" | |
| git add manifest.json manifest.json.sig docs/security/artifacts/ install.sh.sig install.ps1.sig | |
| if git diff --cached --quiet; then | |
| echo "nothing to commit" | |
| exit 0 | |
| fi | |
| git commit -m "release: transparency artifacts ($MODE $VERSION)" | |
| if ! git push origin HEAD:main; then | |
| if [ -n "$VERSION" ]; then | |
| branch="release/v$VERSION" | |
| else | |
| branch="release/$MODE-$(date -u +%Y%m%d%H%M%S)" | |
| fi | |
| git push origin "HEAD:refs/heads/$branch" | |
| if ! gh pr create --base main --head "$branch" \ | |
| --title "release: transparency artifacts ($MODE $VERSION)" \ | |
| --body "Opened by the release workflow because direct push to main is protected. Merge to sync the root manifest and docs/security/artifacts."; then | |
| echo "::warning::could not open the PR automatically; allow GitHub Actions to create pull requests in repo settings or open it manually: https://github.com/$GITHUB_REPOSITORY/pull/new/$branch" | |
| fi | |
| fi |