Skip to content

Release

Release #4

Workflow file for this run

# SimDeck Release Workflow
#
# Triggered manually from the Actions tab. Picks one package, bumps its version
# locally with `npm version`, commits + tags, builds, and publishes:
# - npm packages publish over OIDC trusted publishing (no NPM_TOKEN).
# - The VS Code extension publishes via vsce + VSCE_PAT (no OIDC option).
#
# OIDC setup (do once per npm package on npmjs.com):
# Package -> Settings -> Trusted Publishers -> GitHub Actions
# Repository: <owner>/SimDeck
# Workflow: release.yml
# Environment: npm-publish
#
# Trusted publishers must be configured for each of:
# simdeck
# react-native-simdeck
# @nativescript/simdeck-inspector
#
# Repository secrets / vars:
# VSCE_PAT (only required when publishing simdeck-vscode)
#
# Tag scheme:
# <slug>-v<version> e.g. simdeck-v0.2.0
# react-native-simdeck-v0.2.0
# nativescript-simdeck-inspector-v0.2.0
# simdeck-vscode-v0.2.0
name: Release
on:
workflow_dispatch:
inputs:
package:
description: "Package to release"
required: true
type: choice
options:
- simdeck
- react-native-simdeck
- "@nativescript/simdeck-inspector"
- simdeck-vscode
bump:
description: "Version bump"
required: true
type: choice
default: prerelease
options:
- prerelease
- patch
- minor
- major
preid:
description: "Prerelease identifier (only used when bump=prerelease)"
required: false
type: string
default: next
dist-tag:
description: "npm dist-tag (default: prerelease->preid, otherwise 'latest')"
required: false
type: string
default: ""
dry-run:
description: "Skip git push and publish; print what would happen"
required: false
type: boolean
default: false
concurrency:
group: simdeck-release-${{ inputs.package }}
cancel-in-progress: false
permissions:
contents: write # commit + tag + GitHub release
id-token: write # OIDC trusted publishing + npm provenance
jobs:
release:
name: Release ${{ inputs.package }} (${{ inputs.bump }})
runs-on: macos-latest
environment:
name: ${{ inputs.dry-run && 'npm-publish-dry-run' || 'npm-publish' }}
steps:
- name: Harden the runner (audit egress)
uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1
with:
egress-policy: audit
- name: Checkout (full history)
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org
cache: npm
cache-dependency-path: |
package-lock.json
client/package-lock.json
packages/react-native-inspector/package-lock.json
packages/nativescript-inspector/package-lock.json
- name: Update npm (required for OIDC trusted publishing)
run: |
npm install -g npm@^11.5.1
npm --version
- name: Resolve package metadata
id: meta
shell: bash
env:
INPUT_PACKAGE: ${{ inputs.package }}
run: |
set -euo pipefail
case "$INPUT_PACKAGE" in
simdeck)
dir="."
slug="simdeck"
kind="npm-cli"
;;
react-native-simdeck)
dir="packages/react-native-inspector"
slug="react-native-simdeck"
kind="npm"
;;
"@nativescript/simdeck-inspector")
dir="packages/nativescript-inspector"
slug="nativescript-simdeck-inspector"
kind="npm"
;;
simdeck-vscode)
dir="packages/vscode-extension"
slug="simdeck-vscode"
kind="vsce"
;;
*)
echo "Unknown package input: $INPUT_PACKAGE" >&2
exit 1
;;
esac
echo "dir=$dir" >> "$GITHUB_OUTPUT"
echo "slug=$slug" >> "$GITHUB_OUTPUT"
echo "kind=$kind" >> "$GITHUB_OUTPUT"
- name: Setup Rust toolchain (root simdeck only)
if: ${{ steps.meta.outputs.kind == 'npm-cli' }}
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin
- name: Install root dependencies
run: npm ci
- name: Install client dependencies (root simdeck only)
if: ${{ steps.meta.outputs.kind == 'npm-cli' }}
run: npm ci --prefix client
- name: Install package dependencies
if: ${{ steps.meta.outputs.kind == 'npm' }}
run: npm ci --prefix "${{ steps.meta.outputs.dir }}"
- name: Configure git author
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Bump version
id: version
shell: bash
env:
BUMP: ${{ inputs.bump }}
PREID: ${{ inputs.preid }}
PKG_DIR: ${{ steps.meta.outputs.dir }}
SLUG: ${{ steps.meta.outputs.slug }}
run: |
set -euo pipefail
cd "$PKG_DIR"
if [[ "$BUMP" == "prerelease" ]]; then
new="$(npm version prerelease --preid "$PREID" --no-git-tag-version)"
else
new="$(npm version "$BUMP" --no-git-tag-version)"
fi
version="${new#v}"
tag="${SLUG}-v${version}"
echo "version=${version}" >> "$GITHUB_OUTPUT"
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
echo "Bumped to ${version} (tag ${tag})"
- name: Resolve dist-tag
id: tag
shell: bash
env:
INPUT_TAG: ${{ inputs.dist-tag }}
BUMP: ${{ inputs.bump }}
PREID: ${{ inputs.preid }}
run: |
set -euo pipefail
if [[ -n "$INPUT_TAG" ]]; then
value="$INPUT_TAG"
elif [[ "$BUMP" == "prerelease" ]]; then
value="${PREID:-next}"
else
value="latest"
fi
echo "dist_tag=${value}" >> "$GITHUB_OUTPUT"
- name: Commit and push version bump
if: ${{ !inputs.dry-run }}
shell: bash
env:
PKG_DIR: ${{ steps.meta.outputs.dir }}
INPUT_PACKAGE: ${{ inputs.package }}
NEW_VERSION: ${{ steps.version.outputs.version }}
TAG: ${{ steps.version.outputs.tag }}
run: |
set -euo pipefail
git add "${PKG_DIR}/package.json"
if [[ -f "${PKG_DIR}/package-lock.json" ]]; then
git add "${PKG_DIR}/package-lock.json"
fi
git commit -m "chore(release): ${INPUT_PACKAGE}@${NEW_VERSION}"
git tag "${TAG}"
git push origin HEAD:${{ github.ref_name }}
git push origin "${TAG}"
# ---------- Build (kind-specific) ----------
- name: Build root simdeck (arm64 native binary + client + simdeck-test)
if: ${{ steps.meta.outputs.kind == 'npm-cli' }}
env:
SIMDECK_BUILD_TARGET: aarch64-apple-darwin
run: |
npm run build:cli
npm run build:client
npm run build:simdeck-test
- name: Verify CLI artifact is an arm64 Mach-O
if: ${{ steps.meta.outputs.kind == 'npm-cli' }}
run: |
test -x build/simdeck-bin
file build/simdeck-bin
file build/simdeck-bin | grep -q 'arm64'
# ---------- Apple codesign + notarize (root simdeck only) ----------
- name: Detect Apple signing capability
if: ${{ steps.meta.outputs.kind == 'npm-cli' }}
id: signing
shell: bash
env:
HAS_CERT: ${{ secrets.APPLE_CERT_P12_BASE64 != '' }}
HAS_NOTARY: ${{ secrets.APPLE_NOTARY_KEY_BASE64 != '' }}
HAS_IDENTITY: ${{ vars.APPLE_SIGNING_IDENTITY != '' }}
run: |
set -euo pipefail
if [[ "$HAS_CERT" == "true" && "$HAS_NOTARY" == "true" && "$HAS_IDENTITY" == "true" ]]; then
echo "enabled=true" >> "$GITHUB_OUTPUT"
echo "Apple signing + notarization enabled."
else
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "::warning::Apple signing secrets/vars missing in this environment. The published binary will be UNSIGNED."
fi
- name: Setup signing keychain
if: ${{ steps.signing.outputs.enabled == 'true' }}
shell: bash
env:
APPLE_CERT_P12_BASE64: ${{ secrets.APPLE_CERT_P12_BASE64 }}
APPLE_CERT_P12_PASSWORD: ${{ secrets.APPLE_CERT_P12_PASSWORD }}
run: |
set -euo pipefail
KEYCHAIN_PATH="$RUNNER_TEMP/simdeck-signing.keychain-db"
KEYCHAIN_PASSWORD="$(uuidgen)"
CERT_PATH="$RUNNER_TEMP/cert.p12"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
printf '%s' "$APPLE_CERT_P12_BASE64" | base64 --decode > "$CERT_PATH"
security import "$CERT_PATH" \
-k "$KEYCHAIN_PATH" \
-P "$APPLE_CERT_P12_PASSWORD" \
-T /usr/bin/codesign \
-T /usr/bin/security
security set-key-partition-list \
-S apple-tool:,apple:,codesign: \
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" >/dev/null
# Add temp keychain to user search list, preserving existing entries.
EXISTING_KEYCHAINS="$(security list-keychains -d user | sed -e 's/^[[:space:]]*//' -e 's/"//g')"
# shellcheck disable=SC2086
security list-keychains -d user -s "$KEYCHAIN_PATH" $EXISTING_KEYCHAINS
rm -f "$CERT_PATH"
echo "Signing keychain ready at $KEYCHAIN_PATH"
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
- name: Codesign simdeck binary
if: ${{ steps.signing.outputs.enabled == 'true' }}
shell: bash
env:
APPLE_SIGNING_IDENTITY: ${{ vars.APPLE_SIGNING_IDENTITY }}
run: |
set -euo pipefail
KEYCHAIN_PATH="$RUNNER_TEMP/simdeck-signing.keychain-db"
codesign --force \
--options runtime \
--timestamp \
--keychain "$KEYCHAIN_PATH" \
--sign "$APPLE_SIGNING_IDENTITY" \
--entitlements scripts/release/simdeck.entitlements \
build/simdeck-bin
codesign --verify --strict --verbose=2 build/simdeck-bin
echo "--- Signature details ---"
codesign -dv --verbose=4 build/simdeck-bin
- name: Notarize simdeck binary
if: ${{ steps.signing.outputs.enabled == 'true' && !inputs.dry-run }}
shell: bash
env:
APPLE_NOTARY_KEY_BASE64: ${{ secrets.APPLE_NOTARY_KEY_BASE64 }}
APPLE_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }}
APPLE_NOTARY_KEY_ISSUER_ID: ${{ secrets.APPLE_NOTARY_KEY_ISSUER_ID }}
run: |
set -euo pipefail
NOTARY_KEY_PATH="$RUNNER_TEMP/notary.p8"
NOTARY_ZIP="$RUNNER_TEMP/simdeck-bin.zip"
NOTARY_RESULT="$RUNNER_TEMP/notary-result.json"
printf '%s' "$APPLE_NOTARY_KEY_BASE64" | base64 --decode > "$NOTARY_KEY_PATH"
chmod 600 "$NOTARY_KEY_PATH"
ditto -c -k --keepParent build/simdeck-bin "$NOTARY_ZIP"
set +e
xcrun notarytool submit "$NOTARY_ZIP" \
--key "$NOTARY_KEY_PATH" \
--key-id "$APPLE_NOTARY_KEY_ID" \
--issuer "$APPLE_NOTARY_KEY_ISSUER_ID" \
--wait \
--timeout 30m \
--output-format json > "$NOTARY_RESULT"
NOTARY_STATUS=$?
set -e
echo "--- Notary submission result ---"
cat "$NOTARY_RESULT" || true
if [[ $NOTARY_STATUS -ne 0 ]]; then
SUBMISSION_ID="$(node -e 'try{const j=require(process.argv[1]);process.stdout.write(j.id||"")}catch(e){}' "$NOTARY_RESULT" 2>/dev/null || true)"
if [[ -n "${SUBMISSION_ID:-}" ]]; then
echo "--- Notary log for ${SUBMISSION_ID} ---"
xcrun notarytool log "$SUBMISSION_ID" \
--key "$NOTARY_KEY_PATH" \
--key-id "$APPLE_NOTARY_KEY_ID" \
--issuer "$APPLE_NOTARY_KEY_ISSUER_ID" || true
fi
rm -f "$NOTARY_KEY_PATH" "$NOTARY_ZIP" "$NOTARY_RESULT"
exit "$NOTARY_STATUS"
fi
rm -f "$NOTARY_KEY_PATH" "$NOTARY_ZIP" "$NOTARY_RESULT"
- name: Cleanup signing keychain
if: ${{ always() && steps.signing.outputs.enabled == 'true' }}
shell: bash
run: |
KEYCHAIN_PATH="$RUNNER_TEMP/simdeck-signing.keychain-db"
if [[ -f "$KEYCHAIN_PATH" ]]; then
security delete-keychain "$KEYCHAIN_PATH" || true
fi
rm -f \
"$RUNNER_TEMP/cert.p12" \
"$RUNNER_TEMP/notary.p8" \
"$RUNNER_TEMP/simdeck-bin.zip" \
"$RUNNER_TEMP/notary-result.json" || true
- name: Build npm package
if: ${{ steps.meta.outputs.kind == 'npm' }}
run: npm --prefix "${{ steps.meta.outputs.dir }}" run build
- name: Package VS Code extension
if: ${{ steps.meta.outputs.kind == 'vsce' }}
run: npm run package:vscode-extension
# ---------- Publish (npm OIDC) ----------
- name: Publish to npm (OIDC, dry-run)
if: ${{ inputs.dry-run && (steps.meta.outputs.kind == 'npm' || steps.meta.outputs.kind == 'npm-cli') }}
shell: bash
env:
NPM_CONFIG_PROVENANCE: "true"
NODE_AUTH_TOKEN: ""
DIST_TAG: ${{ steps.tag.outputs.dist_tag }}
PKG_DIR: ${{ steps.meta.outputs.dir }}
SIMDECK_BUILD_TARGET: aarch64-apple-darwin
run: |
set -euo pipefail
unset NODE_AUTH_TOKEN
rm -f ~/.npmrc || true
if [[ -n "${NPM_CONFIG_USERCONFIG:-}" ]]; then
rm -f "$NPM_CONFIG_USERCONFIG" || true
fi
cd "$PKG_DIR"
npm publish --provenance --access public --tag "$DIST_TAG" --dry-run
- name: Publish to npm (OIDC)
if: ${{ !inputs.dry-run && (steps.meta.outputs.kind == 'npm' || steps.meta.outputs.kind == 'npm-cli') }}
shell: bash
env:
NPM_CONFIG_PROVENANCE: "true"
NODE_AUTH_TOKEN: ""
DIST_TAG: ${{ steps.tag.outputs.dist_tag }}
PKG_DIR: ${{ steps.meta.outputs.dir }}
SIMDECK_BUILD_TARGET: aarch64-apple-darwin
run: |
set -euo pipefail
unset NODE_AUTH_TOKEN
rm -f ~/.npmrc || true
if [[ -n "${NPM_CONFIG_USERCONFIG:-}" ]]; then
rm -f "$NPM_CONFIG_USERCONFIG" || true
fi
cd "$PKG_DIR"
npm publish --provenance --access public --tag "$DIST_TAG"
# ---------- Publish (VS Code Marketplace) ----------
- name: Publish VS Code extension (dry-run)
if: ${{ inputs.dry-run && steps.meta.outputs.kind == 'vsce' }}
run: |
ls -la build/vscode/
echo "[dry-run] would: vsce publish --packagePath build/vscode/simdeck-vscode.vsix"
- name: Publish VS Code extension
if: ${{ !inputs.dry-run && steps.meta.outputs.kind == 'vsce' }}
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
run: |
if [[ -z "${VSCE_PAT:-}" ]]; then
echo "VSCE_PAT secret is required to publish the VS Code extension." >&2
exit 1
fi
npx --no-install vsce publish \
--packagePath build/vscode/simdeck-vscode.vsix \
--pat "$VSCE_PAT" \
--skip-duplicate
# ---------- GitHub Release ----------
- name: Create GitHub release
if: ${{ !inputs.dry-run }}
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.version.outputs.tag }}
INPUT_PACKAGE: ${{ inputs.package }}
NEW_VERSION: ${{ steps.version.outputs.version }}
DIST_TAG: ${{ steps.tag.outputs.dist_tag }}
KIND: ${{ steps.meta.outputs.kind }}
BUMP: ${{ inputs.bump }}
run: |
set -euo pipefail
notes="Released ${INPUT_PACKAGE}@${NEW_VERSION} (dist-tag: ${DIST_TAG})."
assets=()
case "$KIND" in
npm-cli)
assets+=("build/simdeck-bin")
;;
vsce)
assets+=("build/vscode/simdeck-vscode.vsix")
;;
esac
flags=()
if [[ "$BUMP" == "prerelease" ]]; then
flags+=(--prerelease)
fi
gh release create "$TAG" \
${assets[@]+"${assets[@]}"} \
--title "$TAG" \
--notes "$notes" \
${flags[@]+"${flags[@]}"}
- name: Summary
if: always()
env:
PKG: ${{ inputs.package }}
BUMP: ${{ inputs.bump }}
NEW: ${{ steps.version.outputs.version }}
TAG: ${{ steps.version.outputs.tag }}
DIST: ${{ steps.tag.outputs.dist_tag }}
DRY: ${{ inputs.dry-run }}
run: |
{
echo "### SimDeck Release"
echo ""
echo "- Package: \`$PKG\`"
echo "- Bump: \`$BUMP\`"
echo "- Version: \`$NEW\`"
echo "- Tag: \`$TAG\`"
echo "- Dist-tag: \`$DIST\`"
echo "- Dry-run: \`$DRY\`"
} >> "$GITHUB_STEP_SUMMARY"