feat(panel): require email verification for access requests #238
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
| # This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist | |
| # | |
| # Copyright 2022-2024, axodotdev | |
| # SPDX-License-Identifier: MIT or Apache-2.0 | |
| # | |
| # CI that: | |
| # | |
| # * checks for a push to main or manual dispatch | |
| # * builds artifacts with dist (archives, installers, hashes) | |
| # * uploads those artifacts to temporary workflow zip | |
| # * on success, uploads the artifacts to a GitHub Release | |
| # | |
| # Note that the GitHub Release will be created with a generated | |
| # title/body based on your changelogs. | |
| name: Release | |
| permissions: | |
| "contents": "write" | |
| on: | |
| pull_request: | |
| paths: | |
| - '.github/workflows/release.yml' | |
| - 'Cargo.lock' | |
| - 'Cargo.toml' | |
| - 'deploy/**' | |
| - 'dist-workspace.toml' | |
| - 'docker-compose.panel.local.yml' | |
| - 'docker-compose.panel.yml' | |
| - 'install-panel.ps1' | |
| - 'install-panel.sh' | |
| - 'src/**' | |
| - 'stitch.example.toml' | |
| - 'tests/**' | |
| push: | |
| branches: [main] | |
| paths: | |
| - '.github/workflows/release.yml' | |
| - 'Cargo.lock' | |
| - 'Cargo.toml' | |
| - 'deploy/**' | |
| - 'dist-workspace.toml' | |
| - 'docker-compose.panel.local.yml' | |
| - 'docker-compose.panel.yml' | |
| - 'install-panel.ps1' | |
| - 'install-panel.sh' | |
| - 'src/**' | |
| - 'stitch.example.toml' | |
| - 'tests/**' | |
| workflow_dispatch: | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| # Run 'dist plan' (or host) to determine what tasks we need to do | |
| plan: | |
| runs-on: "ubuntu-22.04" | |
| outputs: | |
| val: ${{ steps.plan.outputs.manifest }} | |
| tag: ${{ steps.release.outputs.tag }} | |
| tag-flag: ${{ steps.release.outputs.tag_flag }} | |
| publishing: ${{ steps.release.outputs.publishing }} | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| steps: | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 | |
| with: | |
| persist-credentials: false | |
| submodules: recursive | |
| - name: Install dist | |
| # Checksum-pinned installer — do not pipe an unverified script into sh. | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| url="https://github.com/axodotdev/cargo-dist/releases/download/v0.32.0/cargo-dist-installer.sh" | |
| expect="b657cf8c04a8b7bc28f39d220f7e6dd11bbd2bdb072c552262bd9ccf597261b5" | |
| curl --proto '=https' --tlsv1.2 -fsSL "$url" -o /tmp/cargo-dist-installer.sh | |
| echo "$expect /tmp/cargo-dist-installer.sh" | sha256sum -c - | |
| sh /tmp/cargo-dist-installer.sh | |
| - id: release | |
| name: Resolve release tag | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| version="$(awk ' | |
| /^\[package\]$/ { in_package = 1; next } | |
| /^\[/ { in_package = 0 } | |
| in_package && /^version = / { | |
| gsub(/"/, "", $3) | |
| print $3 | |
| exit | |
| } | |
| ' Cargo.toml)" | |
| tag="v${version}" | |
| echo "tag=${tag}" >> "$GITHUB_OUTPUT" | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| echo "tag_flag=" >> "$GITHUB_OUTPUT" | |
| echo "publishing=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "tag_flag=--tag=${tag}" >> "$GITHUB_OUTPUT" | |
| echo "publishing=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Cache dist | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: cargo-dist-cache | |
| path: ~/.cargo/bin/dist | |
| # Scratch for later jobs in this run. Re-apply after `dist generate`. | |
| retention-days: 1 | |
| # sure would be cool if github gave us proper conditionals... | |
| # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible | |
| # functionality based on whether this is a pull_request, and whether it's from a fork. | |
| # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* | |
| # but also really annoying to build CI around when it needs secrets to work right.) | |
| - id: plan | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [ "${{ steps.release.outputs.publishing }}" = "true" ]; then | |
| dist host --allow-dirty --steps=create "${{ steps.release.outputs.tag_flag }}" --output-format=json > plan-dist-manifest.json | |
| else | |
| dist plan --allow-dirty --output-format=json > plan-dist-manifest.json | |
| fi | |
| echo "dist ran successfully" | |
| cat plan-dist-manifest.json | |
| echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" | |
| - name: "Upload dist-manifest.json" | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: artifacts-plan-dist-manifest | |
| path: plan-dist-manifest.json | |
| retention-days: 1 | |
| # Build and packages all the platform-specific things | |
| build-local-artifacts: | |
| name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) | |
| # Let the initial task tell us to not run (currently very blunt) | |
| needs: | |
| - plan | |
| if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} | |
| strategy: | |
| fail-fast: false | |
| # Target platforms/runners are computed by dist in create-release. | |
| # Each member of the matrix has the following arguments: | |
| # | |
| # - runner: the github runner | |
| # - dist-args: cli flags to pass to dist | |
| # - install-dist: expression to run to install dist on the runner | |
| # | |
| # Typically there will be: | |
| # - 1 "global" task that builds universal installers | |
| # - N "local" tasks that build each platform's binaries and platform-specific installers | |
| matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} | |
| runs-on: ${{ matrix.runner }} | |
| container: ${{ matrix.container && matrix.container.image || null }} | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json | |
| steps: | |
| - name: enable windows longpaths | |
| run: | | |
| git config --global core.longpaths true | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 | |
| with: | |
| persist-credentials: false | |
| submodules: recursive | |
| - name: Setup Node | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| cache-dependency-path: web/package-lock.json | |
| - name: Build panel frontend | |
| working-directory: web | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| npm ci --no-audit --no-fund | |
| npm run build | |
| - name: Install Rust non-interactively if not already installed | |
| if: ${{ matrix.container }} | |
| run: | | |
| set -euo pipefail | |
| if ! command -v cargo > /dev/null 2>&1; then | |
| # Pin rustup-init by versioned archive URL + digest. The rolling | |
| # /rustup/dist/… URL changes when rustup ships; archive/1.29.0 stays | |
| # put. Bump RUSTUP_VERSION and both digests together. | |
| RUSTUP_VERSION=1.29.0 | |
| arch="$(uname -m)" | |
| case "$arch" in | |
| x86_64) | |
| triple="x86_64-unknown-linux-gnu" | |
| expect="4acc9acc76d5079515b46346a485974457b5a79893cfb01112423c89aeb5aa10" | |
| ;; | |
| aarch64|arm64) | |
| triple="aarch64-unknown-linux-gnu" | |
| expect="9732d6c5e2a098d3521fca8145d826ae0aaa067ef2385ead08e6feac88fa5792" | |
| ;; | |
| *) | |
| echo "unsupported arch for pinned rustup-init: $arch" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| url="https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/${triple}/rustup-init" | |
| curl --proto '=https' --tlsv1.2 -fsSL "$url" -o /tmp/rustup-init | |
| echo "$expect /tmp/rustup-init" | sha256sum -c - | |
| chmod +x /tmp/rustup-init | |
| /tmp/rustup-init -y --no-modify-path | |
| echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" | |
| fi | |
| - name: Install dist | |
| run: ${{ matrix.install_dist.run }} | |
| # Get the dist-manifest | |
| - name: Fetch local artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| pattern: artifacts-* | |
| path: target/distrib/ | |
| merge-multiple: true | |
| - name: Force apt onto IPv4 | |
| # GitHub's arm64 Linux runners resolve ports.ubuntu.com to IPv6 but have | |
| # no IPv6 route ("Network is unreachable"), which breaks the apt install | |
| # below (the aarch64-unknown-linux-gnu release target fails fetching the | |
| # GTK/X11 dev libs). Pin apt to IPv4. No-op on x86 (archive.ubuntu.com is | |
| # v4) and skipped on non-Linux where there's no apt. | |
| # NOTE: manual patch on a dist-autogenerated file — also covered by | |
| # `github-build-setup = "../build-setup.yml"` for the frontend steps above; | |
| # re-apply the IPv4 pin if release.yml is regenerated by cargo-dist. | |
| if: runner.os == 'Linux' | |
| run: echo 'Acquire::ForceIPv4 "true";' | sudo tee /etc/apt/apt.conf.d/99force-ipv4 | |
| - name: Install dependencies | |
| run: | | |
| ${{ matrix.packages_install }} | |
| - name: Build artifacts | |
| run: | | |
| # Actually do builds and make zips and whatnot | |
| dist build --allow-dirty ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json | |
| echo "dist ran successfully" | |
| - id: cargo-dist | |
| name: Post-build | |
| # We force bash here just because github makes it really hard to get values up | |
| # to "real" actions without writing to env-vars, and writing to env-vars has | |
| # inconsistent syntax between shell and powershell. | |
| shell: bash | |
| run: | | |
| print_upload_paths() { | |
| while IFS= read -r path; do | |
| [ -n "$path" ] || continue | |
| if command -v cygpath >/dev/null 2>&1; then | |
| path="$(cygpath -u "$path")" | |
| fi | |
| case "$path" in | |
| "$PWD"/*) path="${path#"$PWD"/}" ;; | |
| esac | |
| printf '%s\n' "$path" | |
| done | |
| } | |
| # Parse out what we just built and upload it to scratch storage | |
| echo "paths<<EOF" >> "$GITHUB_OUTPUT" | |
| dist print-upload-files-from-manifest --manifest dist-manifest.json | print_upload_paths >> "$GITHUB_OUTPUT" | |
| echo "EOF" >> "$GITHUB_OUTPUT" | |
| cp dist-manifest.json "$BUILD_MANIFEST_NAME" | |
| - name: "Upload artifacts" | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: artifacts-build-local-${{ join(matrix.targets, '_') }} | |
| compression-level: 0 | |
| retention-days: 1 | |
| path: | | |
| ${{ steps.cargo-dist.outputs.paths }} | |
| ${{ env.BUILD_MANIFEST_NAME }} | |
| # Build and package all the platform-agnostic(ish) things | |
| build-global-artifacts: | |
| needs: | |
| - plan | |
| - build-local-artifacts | |
| runs-on: "ubuntu-22.04" | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json | |
| steps: | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 | |
| with: | |
| persist-credentials: false | |
| submodules: recursive | |
| - name: Install cached dist | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: cargo-dist-cache | |
| path: ~/.cargo/bin/ | |
| - run: chmod +x ~/.cargo/bin/dist | |
| # Get all the local artifacts for the global tasks to use (for e.g. checksums) | |
| - name: Fetch local artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| pattern: artifacts-* | |
| path: target/distrib/ | |
| merge-multiple: true | |
| - id: cargo-dist | |
| shell: bash | |
| run: | | |
| print_upload_paths() { | |
| while IFS= read -r path; do | |
| [ -n "$path" ] || continue | |
| if command -v cygpath >/dev/null 2>&1; then | |
| path="$(cygpath -u "$path")" | |
| fi | |
| case "$path" in | |
| "$PWD"/*) path="${path#"$PWD"/}" ;; | |
| esac | |
| printf '%s\n' "$path" | |
| done | |
| } | |
| dist build --allow-dirty ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json | |
| echo "dist ran successfully" | |
| # Parse out what we just built and upload it to scratch storage | |
| echo "paths<<EOF" >> "$GITHUB_OUTPUT" | |
| jq --raw-output ".upload_files[]" dist-manifest.json | print_upload_paths >> "$GITHUB_OUTPUT" | |
| echo "EOF" >> "$GITHUB_OUTPUT" | |
| cp dist-manifest.json "$BUILD_MANIFEST_NAME" | |
| - name: "Upload artifacts" | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: artifacts-build-global | |
| compression-level: 0 | |
| retention-days: 1 | |
| path: | | |
| ${{ steps.cargo-dist.outputs.paths }} | |
| ${{ env.BUILD_MANIFEST_NAME }} | |
| # Determines if we should publish/announce | |
| host: | |
| needs: | |
| - plan | |
| - build-local-artifacts | |
| - build-global-artifacts | |
| # Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine) | |
| if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} | |
| # actions: write is needed for the `gh workflow run` dispatch below; the | |
| # job-level block overrides the workflow default (contents: write only). | |
| permissions: | |
| contents: write | |
| actions: write | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| runs-on: "ubuntu-22.04" | |
| outputs: | |
| val: ${{ steps.host.outputs.manifest }} | |
| steps: | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 | |
| with: | |
| persist-credentials: false | |
| submodules: recursive | |
| - name: Install cached dist | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: cargo-dist-cache | |
| path: ~/.cargo/bin/ | |
| - run: chmod +x ~/.cargo/bin/dist | |
| # Fetch artifacts from scratch-storage | |
| - name: Fetch artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| pattern: artifacts-* | |
| path: target/distrib/ | |
| merge-multiple: true | |
| - id: host | |
| shell: bash | |
| run: | | |
| dist host --allow-dirty ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json | |
| echo "artifacts uploaded and released successfully" | |
| cat dist-manifest.json | |
| echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" | |
| - name: "Upload dist-manifest.json" | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| # Overwrite the previous copy | |
| name: artifacts-dist-manifest | |
| path: dist-manifest.json | |
| retention-days: 1 | |
| # Create a GitHub Release while uploading all files to it | |
| - name: "Download GitHub Artifacts" | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| pattern: artifacts-* | |
| path: artifacts | |
| merge-multiple: true | |
| - name: Cleanup | |
| run: | | |
| # Remove the granular manifests | |
| rm -f artifacts/*-dist-manifest.json | |
| - name: Create GitHub Release | |
| env: | |
| ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" | |
| ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" | |
| RELEASE_COMMIT: "${{ github.sha }}" | |
| run: | | |
| set -euo pipefail | |
| # Write and read notes from a file to avoid quoting breaking things | |
| echo "$ANNOUNCEMENT_BODY" > "$RUNNER_TEMP/notes.txt" | |
| TAG="${{ needs.plan.outputs.tag }}" | |
| # Always create the release flagged as a pre-release, whatever its final | |
| # state should be. GitHub excludes pre-releases (and drafts) from | |
| # /releases/latest, so `latest/download/Stitch.dmg` keeps resolving to | |
| # the PREVIOUS release until the macOS build attaches the DMG below. | |
| # Without this hold, the release becomes `latest` the instant it's | |
| # created — minutes before the DMG lands — so that link 404s for the | |
| # whole build window. The "Promote the release to latest" step clears | |
| # the flag once the DMG is in place. A draft would hide the release | |
| # entirely, but draft releases don't create the git tag, and the macOS | |
| # build is dispatched against it (`--ref "$TAG"`). | |
| # | |
| # `gh release create` is not idempotent. If this job is retried after a | |
| # later step failed (e.g. a transient API error while waiting on the | |
| # macOS build), the release already exists and `create` fails with | |
| # "a release with the same tag name already exists", so the run can | |
| # never go green on a rerun. Detect an existing release and refresh it | |
| # in place instead — re-asserting --prerelease so a rerun never promotes | |
| # it early (promotion happens only in the dedicated step, on success). | |
| if gh release view "$TAG" >/dev/null 2>&1; then | |
| echo "Release $TAG already exists; updating it instead of recreating." | |
| gh release edit "$TAG" --prerelease --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" | |
| gh release upload "$TAG" --clobber artifacts/* | |
| else | |
| gh release create "$TAG" --target "$RELEASE_COMMIT" --prerelease --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* | |
| fi | |
| - name: Upload install-panel integrity checksums | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ needs.plan.outputs.tag }} | |
| run: | | |
| set -euo pipefail | |
| # Operators verify these before `sh install-panel.sh` / | |
| # `install-panel.ps1` (see docs/install-panel.md). | |
| sha256sum install-panel.sh | awk '{print $1 " install-panel.sh"}' > install-panel.sh.sha256 | |
| sha256sum install-panel.ps1 | awk '{print $1 " install-panel.ps1"}' > install-panel.ps1.sha256 | |
| sha256sum docker-compose.panel.yml | awk '{print $1 " docker-compose.panel.yml"}' > docker-compose.panel.yml.sha256 | |
| sha256sum docker-compose.panel.local.yml | awk '{print $1 " docker-compose.panel.local.yml"}' > docker-compose.panel.local.yml.sha256 | |
| gh release upload "$TAG" --clobber \ | |
| install-panel.sh.sha256 \ | |
| install-panel.ps1.sha256 \ | |
| docker-compose.panel.yml.sha256 \ | |
| docker-compose.panel.local.yml.sha256 | |
| # The release above is created with GITHUB_TOKEN, so it does NOT fire the | |
| # `release: published` event. Trigger the macOS app build explicitly | |
| # (workflow_dispatch is exempt from that restriction) so Stitch.dmg gets | |
| # built and attached to this release. | |
| # | |
| # Fire-and-forget on purpose: we do NOT wait for the ~12-minute macOS build | |
| # here. Blocking the release job that long serialized releases — the | |
| # workflow has `concurrency: cancel-in-progress: false`, so two merges | |
| # within that window queued up and GitHub cancelled the one in the middle. | |
| # Instead, macos-app.yml attaches the DMG and, when promote=true, clears the | |
| # pre-release flag to make this the latest release itself. A failed macOS | |
| # build then just leaves the release as a pre-release (never `latest`, so | |
| # the latest/download link keeps pointing at the previous release rather | |
| # than 404ing) and shows red on the macos-app.yml run. | |
| - name: Build and attach the macOS Stitch.dmg | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ needs.plan.outputs.tag }} | |
| # Promote to `latest` once the DMG lands, unless dist flagged this a | |
| # genuine pre-release (those are never meant to become `latest`). | |
| PROMOTE: ${{ !fromJson(steps.host.outputs.manifest).announcement_is_prerelease && 'true' || 'false' }} | |
| run: | | |
| set -euo pipefail | |
| gh workflow run macos-app.yml --ref "$TAG" -f tag="$TAG" -f promote="$PROMOTE" | |
| echo "Dispatched the macOS app build for $TAG (promote=$PROMOTE); it attaches the DMG and promotes the release when done." | |
| announce: | |
| needs: | |
| - plan | |
| - host | |
| # use "always() && ..." to allow us to wait for all publish jobs while | |
| # still allowing individual publish jobs to skip themselves (for prereleases). | |
| # "host" however must run to completion, no skipping allowed! | |
| if: ${{ always() && needs.host.result == 'success' }} | |
| runs-on: "ubuntu-22.04" | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| steps: | |
| - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 | |
| with: | |
| persist-credentials: false | |
| submodules: recursive |