name: Build Snap Package on: # workflow_run only works if this workflow file lives on the DEFAULT branch. # Triggered as soon as "Release" finishes (which itself triggers on # `release: published` + workflow_dispatch, not on every push to main). workflow_run: workflows: ["Release"] types: [completed] workflow_dispatch: inputs: version: description: 'Existing release tag to build against (test)' required: true default: 'v0.9.0' # Minimal default; the build-snap job raises this to `contents: write` itself, # because it attaches the snap as a release asset. permissions: contents: read # One global group, and deliberately NOT cancel-in-progress: two releases in # quick succession would both publish to the Snap Store's `stable` channel, and # a Snap release cannot be retracted. concurrency: group: snap cancel-in-progress: false jobs: build-snap: timeout-minutes: 75 runs-on: ubuntu-22.04 # `types: [completed]` also fires when the release workflow FAILED or was # cancelled; without this gate a failed release would still build a snap. # No extra head_branch guard is needed here: "Release" only triggers on # `release: published` + workflow_dispatch, never on an ordinary push to # main, so there is no spurious-trigger scenario. if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' permissions: contents: write env: # Job-level so the gated publish step can reference it from `if:`. SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} steps: - uses: actions/checkout@v5 with: # On workflow_run, checkout defaults to the DEFAULT BRANCH rather than # the tag/commit the release workflow built — wrong, because # snap/snapcraft.yaml must come from the built commit. Hence the # explicit head_sha of that workflow; for workflow_dispatch (no # workflow_run event) it falls back to github.ref, i.e. the branch the # manual dispatch was started from. ref: ${{ github.event.workflow_run.head_sha || github.ref }} - name: Get version id: get-version env: # Untrusted input goes through env, NEVER directly as ${} in the shell. # Do not set GITHUB_* names here: they are reserved and already present. EVENT_NAME: ${{ github.event_name }} DISPATCH_VERSION: ${{ github.event.inputs.version }} HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | if [ "$EVENT_NAME" = "workflow_dispatch" ]; then VERSION="$DISPATCH_VERSION" elif [ "$EVENT_NAME" = "workflow_run" ]; then # For a `release`-triggered workflow_run, head_branch is not # guaranteed to be the tag name (it can be the default branch), so # fall back to looking up the latest published release instead of # trusting head_branch. if printf '%s' "$HEAD_BRANCH" | grep -qE '^v[0-9]'; then VERSION="$HEAD_BRANCH" else VERSION="$(gh release list --exclude-drafts --limit 1 --json tagName --jq '.[0].tagName // empty')" fi else VERSION="" fi if [ -z "$VERSION" ]; then echo "::error::Could not determine version (event=$EVENT_NAME, head_branch=$HEAD_BRANCH)" exit 1 fi # `version` and `version_number` are fed into a `sed` REPLACEMENT # PATTERN below, and a dispatch input is free text — a '/' or '&' would # rewrite that pattern, and a newline would write a second line into # $GITHUB_OUTPUT (output injection). Whitelist: the `case` covers the # entire value (including newlines), the regex then checks structure. case "$VERSION" in *[!v0-9.]*) echo "::error::Version contains characters other than 'v', digits and dots." >&2 exit 1 ;; esac if ! printf '%s' "$VERSION" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then echo "::error::Version '$VERSION' is not of the form vX.Y.Z." >&2 exit 1 fi echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "version_number=${VERSION#v}" >> "$GITHUB_OUTPUT" # We do NOT rebuild the Rust app: the snap repackages the AppImage from the # published GitHub release (--appimage-extract rather than dpkg-deb -x, # since this project does not produce a .deb). - name: Wait for release AppImage and download it env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ steps.get-version.outputs.version }} run: | echo "Waiting for AppImage asset of release $VERSION..." found="" for i in $(seq 1 10); do if gh release view "$VERSION" --json assets --jq '.assets[].name' 2>/dev/null | grep -q '\.AppImage$'; then found=1 echo "AppImage asset found" break fi echo "no AppImage asset yet... (attempt $i/10)" sleep 15 done if [ -z "$found" ]; then echo "::error::AppImage asset for $VERSION did not appear in time" exit 1 fi gh release download "$VERSION" -p '*.AppImage' --clobber --dir . appimage=$(ls *.AppImage | head -1) cp "$appimage" open-cad-studio.AppImage echo "Using $appimage -> open-cad-studio.AppImage" - name: Set version in snapcraft.yaml env: VERSION_NUMBER: ${{ steps.get-version.outputs.version_number }} run: | sed -i "s/^version: .*/version: '$VERSION_NUMBER'/" snap/snapcraft.yaml grep '^version:' snap/snapcraft.yaml - name: Build snap uses: snapcore/action-build@v1 id: snapcraft # Real release (via workflow_run): attach the snap as a release asset. - name: Upload snap to GitHub release if: github.event_name == 'workflow_run' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ steps.get-version.outputs.version }} SNAP: ${{ steps.snapcraft.outputs.snap }} run: | gh release upload "$VERSION" "$SNAP" --clobber # Manual test run: snap as a CI artifact only, do NOT touch the release. - name: Upload snap as workflow artifact if: github.event_name == 'workflow_dispatch' uses: actions/upload-artifact@v4 with: name: open-cad-studio-snap path: ${{ steps.snapcraft.outputs.snap }} if-no-files-found: error # Publishing live to the Snap Store: only on a real release, and only once # the store credentials secret exists (an owner step) — and only once the # name `open-cad-studio` has been registered on snapcraft.io. - name: Publish to Snap Store if: github.event_name == 'workflow_run' && env.SNAPCRAFT_STORE_CREDENTIALS != '' uses: snapcore/action-publish@v1 with: snap: ${{ steps.snapcraft.outputs.snap }} release: stable