Skip to content

feat: allow importing stitch.toml in bot setup wizard #232

feat: allow importing stitch.toml in bot setup wizard

feat: allow importing stitch.toml in bot setup wizard #232

Workflow file for this run

# 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