deps(npm): bump the npm-all group with 11 updates #171
Workflow file for this run
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: Preview Build | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| # Produces a fully signed + notarised macOS build from any branch or PR so the | |
| # team can validate the artifact before tagging an official release. | |
| # | |
| # Two ways to trigger: | |
| # | |
| # 1. Add the "build" label to a pull request. | |
| # → Checks out the PR head, builds, posts a download-link comment on the PR. | |
| # | |
| # 2. Actions → "Preview Build" → "Run workflow" (workflow_dispatch). | |
| # → Select the branch (and optionally paste a full SHA in the input). | |
| # Use this to test any branch, not just open PRs. | |
| # | |
| # The result is uploaded as a GitHub Actions artifact (14-day retention). | |
| # No GitHub Release is created, so the Releases page stays clean. | |
| # | |
| # Artifact name: skill-preview-pr<N>-<sha8> (label trigger) | |
| # skill-preview-<branch>-<sha8> (dispatch trigger) | |
| # | |
| # ── Security hardening ──────────────────────────────────────────────────────── | |
| # | |
| # Third-party (fork) PRs are blocked at three independent layers: | |
| # | |
| # 1. Job condition — skips the entire job if head.repo ≠ this repo. | |
| # 2. Labeler check — verifies the actor who added the label has ≥ write | |
| # permission even for same-repo PRs. | |
| # 3. Secret gate — asserts all Release secrets are present BEFORE any | |
| # PR code is checked out or executed. | |
| # | |
| # A fourth layer (GitHub UI) must be configured manually: | |
| # Settings → Environments → Release → Required reviewers | |
| # Add one or two trusted maintainers so GitHub pauses the job and requires a | |
| # human approval click before any environment secret is released. | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| on: | |
| # ── Add the "build" label to a PR ────────────────────────────────────────── | |
| pull_request: | |
| types: [labeled] | |
| # ── Actions → Run workflow ────────────────────────────────────────────────── | |
| workflow_dispatch: | |
| inputs: | |
| ref: | |
| description: >- | |
| Branch, tag, or full SHA to build. | |
| Leave blank to build the branch selected in the "Run workflow" dropdown. | |
| required: false | |
| default: '' | |
| # One preview build per PR at a time — cancel the running one when a new | |
| # commit is pushed or the label is re-applied. | |
| concurrency: | |
| group: preview-${{ github.event.pull_request.number || github.run_id }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read # checkout | |
| pull-requests: write # post download-link comment | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| jobs: | |
| preview: | |
| # ── [FIX 1] Fork rejection ──────────────────────────────────────────────── | |
| # For label events: only run when | |
| # a) the label is literally "build", AND | |
| # b) the PR originates from this repo (not a fork). | |
| # Forks do not receive environment secrets under pull_request, but they | |
| # can still execute arbitrary code on the runner before the secret check. | |
| # Rejecting them here stops the job before a single line of PR code runs. | |
| # workflow_dispatch events always originate from a maintainer — always run. | |
| if: >- | |
| github.event_name == 'workflow_dispatch' || | |
| ( | |
| github.event_name == 'pull_request' && | |
| github.event.label.name == 'build' && | |
| github.event.pull_request.head.repo.full_name == github.repository | |
| ) | |
| name: Preview — aarch64-apple-darwin | |
| runs-on: macos-26 | |
| # Pull signing secrets from the same environment as the official release. | |
| environment: Release | |
| steps: | |
| # ── [FIX 2] Verify the labeler has write (or higher) permission ───────── | |
| # | |
| # Labels can only be added by collaborators, but a repo may grant "read" | |
| # or "triage" to outside collaborators who can still apply labels. | |
| # This step confirms the actor has at minimum "write" access before the | |
| # job proceeds. Runs only for label events; dispatch is always a | |
| # maintainer. | |
| - name: Verify labeler has write access | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| username: context.actor, | |
| }); | |
| const level = data.permission; // none|read|triage|write|maintain|admin | |
| const allowed = ['write', 'maintain', 'admin']; | |
| if (!allowed.includes(level)) { | |
| core.setFailed( | |
| `Actor '${context.actor}' has permission '${level}' — ` + | |
| `only write / maintain / admin may trigger preview builds.` | |
| ); | |
| } | |
| console.log(`✓ Actor '${context.actor}' has permission '${level}'.`); | |
| # ── [FIX 3] Verify signing secrets BEFORE any PR code is executed ─────── | |
| # | |
| # The original workflow checked secrets after npm ci and build-espeak-static.sh, | |
| # meaning PR code ran first. By asserting secrets here — before checkout — | |
| # we guarantee no untrusted code ever touches the runner if secrets are absent. | |
| # | |
| # Fail fast before the 20-minute build if any secret is missing. | |
| # Values are never printed. | |
| - name: Verify signing and notarization secrets | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| run: >- | |
| node scripts/ci.mjs verify-secrets | |
| APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY | |
| APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID TAURI_SIGNING_PRIVATE_KEY | |
| # ── Resolve what to check out ─────────────────────────────────────────── | |
| # | |
| # For label events → use the PR head SHA (immutable reference). | |
| # For dispatch → use inputs.ref if provided, else github.ref | |
| # (the branch selected in the "Run workflow" dropdown). | |
| - name: Resolve ref and PR metadata | |
| id: meta | |
| run: | | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| echo "ref=${{ github.event.pull_request.head.sha }}" >> "$GITHUB_OUTPUT" | |
| echo "pr=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT" | |
| echo "branch=${{ github.event.pull_request.head.ref }}" >> "$GITHUB_OUTPUT" | |
| else | |
| REF="${{ inputs.ref }}" | |
| echo "ref=${REF:-${{ github.ref }}}" >> "$GITHUB_OUTPUT" | |
| echo "pr=" >> "$GITHUB_OUTPUT" | |
| echo "branch=${{ github.ref_name }}" >> "$GITHUB_OUTPUT" | |
| fi | |
| # ── Checkout ──────────────────────────────────────────────────────────── | |
| # Reached only after all three security gates above have passed. | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ steps.meta.outputs.ref }} | |
| fetch-depth: 0 | |
| # ── Build label (version string used in artifact names + PR comment) ──── | |
| - name: Compute build label | |
| id: label | |
| run: | | |
| SHA=$(git rev-parse --short=8 HEAD) | |
| PR="${{ steps.meta.outputs.pr }}" | |
| BRANCH="${{ steps.meta.outputs.branch }}" | |
| CONF_VERSION=$( | |
| grep '"version"' src-tauri/tauri.conf.json \ | |
| | head -1 \ | |
| | sed 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' | |
| ) | |
| if [ -n "$PR" ]; then | |
| ARTIFACT_NAME="skill-preview-pr${PR}-${SHA}" | |
| BUILD_TITLE="Preview — PR #${PR} (${BRANCH}) @ ${SHA}" | |
| else | |
| SAFE_BRANCH="${BRANCH//\//-}" | |
| ARTIFACT_NAME="skill-preview-${SAFE_BRANCH}-${SHA}" | |
| BUILD_TITLE="Preview — ${BRANCH} @ ${SHA}" | |
| fi | |
| echo "sha=$SHA" >> "$GITHUB_OUTPUT" | |
| echo "artifact_name=$ARTIFACT_NAME" >> "$GITHUB_OUTPUT" | |
| echo "build_title=$BUILD_TITLE" >> "$GITHUB_OUTPUT" | |
| echo "conf_version=$CONF_VERSION" >> "$GITHUB_OUTPUT" | |
| # Also expose as env var so later steps can read it easily. | |
| echo "VERSION=$CONF_VERSION" >> "$GITHUB_ENV" | |
| echo "Building: $BUILD_TITLE" | |
| echo "Artifact: $ARTIFACT_NAME" | |
| echo "Version : $CONF_VERSION" | |
| # ── Rust toolchain ────────────────────────────────────────────────────── | |
| - name: Install Rust stable | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: aarch64-apple-darwin | |
| - name: Cache Cargo registry + build artifacts | |
| uses: Swatinem/[email protected] | |
| with: | |
| workspaces: ". -> src-tauri/target" | |
| # ── Build cache (sccache) ───────────────────────────────────────────────── | |
| - name: Setup sccache | |
| uses: Mozilla-Actions/[email protected] | |
| - name: Configure sccache for GitHub Actions cache | |
| shell: bash | |
| run: | | |
| echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV" | |
| echo "SCCACHE_CACHE_SIZE=10G" >> "$GITHUB_ENV" | |
| echo "SCCACHE_GHA_VERSION=1" >> "$GITHUB_ENV" | |
| # ── Node.js ───────────────────────────────────────────────────────────── | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '22' | |
| cache: 'npm' | |
| - name: Install JS dependencies | |
| run: npm ci | |
| - name: Install protoc + GNU ar (macOS) | |
| run: | | |
| brew install protobuf binutils | |
| # Use GNU ar to suppress "illegal option -- D" warnings from Apple's system ar | |
| echo "AR=$(brew --prefix binutils)/bin/gar" >> "$GITHUB_ENV" | |
| - name: i18n sync check | |
| run: npm run -s sync:i18n:check | |
| - name: i18n audit check | |
| run: npm run -s audit:i18n:check | |
| - name: i18n locales check (all non-en locales) | |
| run: npm run -s check:i18n:locales | |
| # ── Validate Apple notarization credentials ────────────────────────────── | |
| - name: Validate Apple notarization credentials | |
| env: | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: node scripts/ci.mjs validate-notarization | |
| # ── Import Apple Developer certificate into a temporary keychain ───────── | |
| - name: Import Apple Developer certificate | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| run: node scripts/ci.mjs import-apple-cert | |
| # ── Compile: frontend + Rust binary ────────────────────────────────────── | |
| # Identical rationale to release-mac.yml — see that file for full details. | |
| # --features custom-protocol tells Tauri to embed the SvelteKit build | |
| # output and serve it via the custom:// protocol instead of connecting | |
| # to the dev server (localhost:1420). | |
| - name: Compile (frontend + Rust) | |
| env: | |
| RUSTC_WRAPPER: sccache | |
| CMAKE_C_COMPILER_LAUNCHER: sccache | |
| CMAKE_CXX_COMPILER_LAUNCHER: sccache | |
| run: | | |
| npm run build | |
| cargo build -p skill --release --locked --target aarch64-apple-darwin --features custom-protocol | |
| # ── Assemble .app bundle ────────────────────────────────────────────── | |
| - name: Create .app bundle | |
| run: bash scripts/assemble-macos-app.sh aarch64-apple-darwin | |
| # ── Sign .app + create DMG (background, README, CHANGELOG, LICENSE) ───── | |
| # | |
| # Uses sindresorhus/create-dmg via scripts/create-macos-dmg.sh which | |
| # signs .app, creates DMG with composed icon + Retina background + SLA, | |
| # signs the DMG, and notarizes with Apple. | |
| - name: Install DMG dependencies | |
| run: | | |
| npm install --global appdmg | |
| pip3 install --quiet Pillow | |
| - name: Sign .app + Create DMG | |
| env: | |
| APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: bash scripts/create-macos-dmg.sh aarch64-apple-darwin | |
| # ── Create updater artifact from the signed + stapled .app ───────────── | |
| # Runs after DMG so the .app is already notarized and stapled. | |
| - name: Recreate updater artifact from signed .app | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| run: | | |
| BUNDLE_DIR="src-tauri/target/aarch64-apple-darwin/release/bundle" | |
| APP_BUNDLE="$(find "$BUNDLE_DIR/macos" -name "*.app" -maxdepth 1 | head -1)" | |
| find "$BUNDLE_DIR/macos" \ | |
| \( -name "*.app.tar.gz" -o -name "*.app.tar.gz.sig" \) -delete | |
| TAR_PATH="$BUNDLE_DIR/macos/$(basename "$APP_BUNDLE").tar.gz" | |
| tar -czf "$TAR_PATH" \ | |
| -C "$(dirname "$APP_BUNDLE")" "$(basename "$APP_BUNDLE")" | |
| npx tauri signer sign -f "$TAR_PATH" | |
| # ── Always delete the temporary keychain ───────────────────────────────── | |
| - name: Delete temporary keychain | |
| if: always() | |
| run: security delete-keychain "$KEYCHAIN_PATH" 2>/dev/null || true | |
| # ── Locate build artifacts ─────────────────────────────────────────────── | |
| - name: Collect artifact paths | |
| id: artifacts | |
| run: | | |
| BUNDLE_DIR="src-tauri/target/aarch64-apple-darwin/release/bundle" | |
| APP_TAR="$(find "$BUNDLE_DIR/macos" \ | |
| -name "*.app.tar.gz" ! -name "*.sig" \ | |
| -maxdepth 1 | head -1)" | |
| APP_SIG="$(find "$BUNDLE_DIR/macos" \ | |
| -name "*.app.tar.gz.sig" \ | |
| -maxdepth 1 | head -1)" | |
| DMG="$(find "$BUNDLE_DIR/dmg" \ | |
| -name "*.dmg" \ | |
| -maxdepth 1 | head -1)" | |
| if [ -z "$APP_TAR" ] || [ -z "$APP_SIG" ] || [ -z "$DMG" ]; then | |
| echo "::error::One or more expected build artifacts are missing." | |
| echo " APP_TAR : ${APP_TAR:-(not found)}" | |
| echo " APP_SIG : ${APP_SIG:-(not found)}" | |
| echo " DMG : ${DMG:-(not found)}" | |
| exit 1 | |
| fi | |
| echo "app_tar=$APP_TAR" >> "$GITHUB_OUTPUT" | |
| echo "app_sig=$APP_SIG" >> "$GITHUB_OUTPUT" | |
| echo "dmg=$DMG" >> "$GITHUB_OUTPUT" | |
| echo "Artifacts:" | |
| echo " DMG : $DMG ($(du -sh "$DMG" | cut -f1))" | |
| echo " TAR : $APP_TAR ($(du -sh "$APP_TAR" | cut -f1))" | |
| echo " SIG : $APP_SIG" | |
| - name: Prepare changelog notes for preview artifact | |
| run: >- | |
| node scripts/ci.mjs prepare-changelog | |
| "${{ steps.label.outputs.conf_version }}" preview-notes.md | |
| # ── Upload to GitHub Actions artifacts (14-day retention) ──────────────── | |
| # | |
| # Download from: | |
| # Actions → this run → Artifacts section at the bottom of the summary page. | |
| # | |
| # Contents: | |
| # *.dmg — installer; drag to /Applications to test | |
| # *.app.tar.gz — Tauri updater bundle | |
| # *.app.tar.gz.sig — Ed25519 updater signature | |
| - name: Upload preview artifacts | |
| id: upload | |
| uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 | |
| with: | |
| name: ${{ steps.label.outputs.artifact_name }} | |
| retention-days: 14 | |
| if-no-files-found: error | |
| path: | | |
| ${{ steps.artifacts.outputs.dmg }} | |
| ${{ steps.artifacts.outputs.app_tar }} | |
| ${{ steps.artifacts.outputs.app_sig }} | |
| preview-notes.md | |
| # ── Notify Discord of preview build result ─────────────────────────────── | |
| - name: Notify Discord of preview build | |
| if: always() | |
| env: | |
| DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} | |
| JOB_STATUS: ${{ job.status }} | |
| run: | | |
| COMMIT_MSG=$(git log -1 --format='%s' | head -c 200) | |
| RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| SHA="${{ steps.label.outputs.sha }}" | |
| VERSION="${{ steps.label.outputs.conf_version }}" | |
| TITLE="${{ steps.label.outputs.build_title }}" | |
| ARTIFACT="${{ steps.label.outputs.artifact_name }}" | |
| PR="${{ steps.meta.outputs.pr }}" | |
| BRANCH="${{ steps.meta.outputs.branch }}" | |
| if [[ "$JOB_STATUS" == "success" ]]; then | |
| COLOR=3066993 # green | |
| EMOJI="✅" | |
| DESC="Preview build succeeded. Artifact available for 14 days." | |
| else | |
| COLOR=15158332 # red | |
| EMOJI="❌" | |
| DESC="Preview build failed. Check the run for details." | |
| fi | |
| PR_FIELD="" | |
| if [[ -n "$PR" ]]; then | |
| PR_URL="${{ github.server_url }}/${{ github.repository }}/pull/$PR" | |
| PR_FIELD="{\"name\": \"PR\", \"value\": \"[#$PR]($PR_URL)\", \"inline\": true}," | |
| fi | |
| curl -s -X POST "$DISCORD_WEBHOOK_URL" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{ | |
| \"embeds\": [{ | |
| \"title\": \"$EMOJI Preview Build — \`v$VERSION\` @ \`$SHA\`\", | |
| \"description\": \"$DESC\n\n**[View run & download artifact]($RUN_URL)**\", | |
| \"url\": \"$RUN_URL\", | |
| \"color\": $COLOR, | |
| \"fields\": [ | |
| $PR_FIELD | |
| {\"name\": \"Branch\", \"value\": \"\`$BRANCH\`\", \"inline\": true}, | |
| {\"name\": \"Artifact\", \"value\": \"\`$ARTIFACT\`\", \"inline\": true}, | |
| {\"name\": \"Commit Message\", \"value\": $(echo "$COMMIT_MSG" | jq -Rs .), \"inline\": false} | |
| ], | |
| \"footer\": {\"text\": \"${{ github.repository }}\"} | |
| }] | |
| }" | |
| # ── Post (or update) a download-link comment on the PR ─────────────────── | |
| # | |
| # Only runs when triggered by the "build" label on a PR. | |
| # If a previous preview comment exists on this PR it is edited in-place | |
| # rather than creating a new one — keeps the PR thread tidy. | |
| - name: Comment on PR with download link | |
| if: steps.meta.outputs.pr != '' | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| const pr = Number('${{ steps.meta.outputs.pr }}'); | |
| const sha = '${{ steps.label.outputs.sha }}'; | |
| const version = '${{ steps.label.outputs.conf_version }}'; | |
| const title = '${{ steps.label.outputs.build_title }}'; | |
| const name = '${{ steps.label.outputs.artifact_name }}'; | |
| const runId = context.runId; | |
| const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; | |
| // GitHub Actions artifact download links require authentication. | |
| // The run summary page is the best stable public URL. | |
| const downloadUrl = `${runUrl}#artifacts`; | |
| const MARKER = '<!-- skill-preview-build -->'; | |
| const body = [ | |
| MARKER, | |
| `### 🏗️ Preview build ready — \`v${version}\` @ \`${sha}\``, | |
| '', | |
| 'A **signed & notarised** macOS build of this PR is available. ' + | |
| 'Artifacts expire after **14 days**.', | |
| '', | |
| `| | |`, | |
| `|---|---|`, | |
| `| 📦 Download | **[${name}](${downloadUrl})** |`, | |
| `| Run | [#${runId}](${runUrl}) |`, | |
| `| SHA | \`${sha}\` |`, | |
| `| Version | \`${version}\` |`, | |
| '', | |
| '**Installation:** download the `.dmg`, open it, drag the app to `/Applications`.', | |
| '', | |
| '> ⚠️ This is a **pre-release validation build** — not an official release.', | |
| ].join('\n'); | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr, | |
| }); | |
| // Find the previous preview comment by the bot marker, if any. | |
| const existing = comments.find(c => | |
| c.user.type === 'Bot' && c.body.includes(MARKER) | |
| ); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| console.log(`Updated existing comment #${existing.id}`); | |
| } else { | |
| const { data: created } = await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr, | |
| body, | |
| }); | |
| console.log(`Created comment #${created.id}`); | |
| } |