Merge pull request #707 from aidenlx/feat/pandoc-ref #97
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: Release | |
| on: | |
| push: | |
| branches: [main, next] | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| id-token: write | |
| attestations: write | |
| artifact-metadata: write | |
| concurrency: | |
| group: release | |
| cancel-in-progress: false | |
| jobs: | |
| obsidian: | |
| runs-on: ubuntu-latest | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| submodules: recursive | |
| # `mise run init` installs deps (frozen lockfile) and builds packages/*. | |
| # `--no-submodules` skips checkout since actions/checkout already fetched | |
| # them. | |
| - uses: jdx/mise-action@v4 | |
| # pnpm is activated by mise's corepack post-install; this step is only | |
| # here for the store cache. | |
| - uses: pnpm/action-setup@v6 | |
| with: | |
| cache: true | |
| - run: mise run init --no-submodules | |
| - name: Resolve version | |
| id: meta | |
| run: | | |
| version="$(node -p "require('./apps/obsidian/package.json').version")" | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| echo "tag=$version" >> "$GITHUB_OUTPUT" | |
| echo "res_tag=res-$version" >> "$GITHUB_OUTPUT" | |
| prerelease="$(pnpm --filter @zotlit/scripts exec node -p \ | |
| "require('semver').prerelease('$version') !== null")" | |
| echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT" | |
| # Tag existence is the idempotency gate: re-running on the same version is a no-op. | |
| - name: Check release exists | |
| id: gate | |
| run: | | |
| if gh release view "${{ steps.meta.outputs.tag }}" >/dev/null 2>&1; then | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "exists=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Build | |
| if: steps.gate.outputs.exists == 'false' | |
| run: pnpm turbo run build --filter=@zotlit/obsidian | |
| # Everything the installed plugin downloads at runtime, staged into one | |
| # directory so the base locale — which ships bundled inside main.js — is | |
| # skipped once and every later step globs one place. | |
| - name: Stage resources | |
| if: steps.gate.outputs.exists == 'false' | |
| run: | | |
| pnpm turbo run generate:language-packs --filter=@zotlit/obsidian | |
| mkdir -p apps/obsidian/dist/resources | |
| for pack in apps/obsidian/src/lib/i18n/generated/*.json; do | |
| [ "${pack##*/}" = "en.json" ] && continue | |
| cp "$pack" apps/obsidian/dist/resources/ | |
| done | |
| cp packages/db/src/contract/generated/*.schema.json apps/obsidian/dist/resources/ | |
| # The plugin release carries only the three files the Obsidian community | |
| # scanner accepts, so everything else lives on a Resource Release pinned to | |
| # the same version. Created before the plugin release, which is the sole | |
| # gate: while the plugin tag is absent no installed build can name this | |
| # version, so a retry may re-upload assets over a partial earlier run. | |
| - name: Create Resource Release | |
| if: steps.gate.outputs.exists == 'false' | |
| run: | | |
| if ! gh release view "${{ steps.meta.outputs.res_tag }}" >/dev/null 2>&1; then | |
| notes=$(printf '%s\n\n%s\n%s' \ | |
| "Runtime resources for ZotLit Obsidian plugin v${{ steps.meta.outputs.version }}: Language Packs and template data JSON Schemas." \ | |
| "The installed plugin downloads these on its own — there is nothing to install by hand." \ | |
| "Get the plugin from the ${{ steps.meta.outputs.tag }} release.") | |
| gh release create "${{ steps.meta.outputs.res_tag }}" \ | |
| --title "${{ steps.meta.outputs.res_tag }}" \ | |
| --target "${{ github.sha }}" \ | |
| --notes "$notes" \ | |
| ${{ steps.meta.outputs.prerelease == 'true' && '--prerelease' || '' }} \ | |
| --latest=false | |
| fi | |
| gh release upload "${{ steps.meta.outputs.res_tag }}" \ | |
| apps/obsidian/dist/resources/* --clobber | |
| # Downloads each resource anonymously from the versioned URL the plugin | |
| # composes, so a missed or corrupted upload fails the run loudly — before | |
| # the plugin release exists, which keeps the failure free of cleanup. | |
| - name: Verify resources | |
| if: steps.gate.outputs.exists == 'false' | |
| run: | | |
| download_dir="$(mktemp -d)" | |
| for resource in apps/obsidian/dist/resources/*; do | |
| file_name="${resource##*/}" | |
| url="https://github.com/${GITHUB_REPOSITORY}/releases/download/${{ steps.meta.outputs.res_tag }}/$file_name" | |
| for attempt in 1 2 3 4 5; do | |
| if curl --fail --silent --show-error --location \ | |
| --header "Cache-Control: no-cache" \ | |
| --output "$download_dir/$file_name" \ | |
| "$url" && | |
| cmp --silent "$resource" "$download_dir/$file_name"; then | |
| break | |
| fi | |
| if [ "$attempt" -eq 5 ]; then | |
| echo "::error file=$resource::Anonymous download did not match the staged resource" | |
| exit 1 | |
| fi | |
| sleep 2 | |
| done | |
| done | |
| - name: Create release | |
| if: steps.gate.outputs.exists == 'false' | |
| run: | | |
| notes=$(printf '%s\n\n%s' \ | |
| "ZotLit Obsidian plugin v${{ steps.meta.outputs.version }}" \ | |
| "Release notes: https://zotlit.aidenlx.site/changelog/${{ steps.meta.outputs.version }}") | |
| gh release create "${{ steps.meta.outputs.tag }}" \ | |
| --title "${{ steps.meta.outputs.tag }}" \ | |
| --target "${{ github.sha }}" \ | |
| --notes "$notes" \ | |
| ${{ steps.meta.outputs.prerelease == 'true' && '--prerelease' || '--latest' }} \ | |
| apps/obsidian/dist/main.js \ | |
| apps/obsidian/dist/manifest.json \ | |
| apps/obsidian/dist/styles.css | |
| - name: Attest provenance | |
| if: steps.gate.outputs.exists == 'false' | |
| uses: actions/attest@v4 | |
| with: | |
| subject-path: | | |
| apps/obsidian/dist/main.js | |
| apps/obsidian/dist/manifest.json | |
| apps/obsidian/dist/styles.css | |
| apps/obsidian/dist/resources/* | |
| zotero: | |
| runs-on: ubuntu-latest | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| submodules: recursive | |
| # `mise run init` installs deps (frozen lockfile) and builds packages/*. | |
| # `--no-submodules` skips checkout since actions/checkout already fetched | |
| # them. | |
| - uses: jdx/mise-action@v4 | |
| # pnpm is activated by mise's corepack post-install; this step is only | |
| # here for the store cache. | |
| - uses: pnpm/action-setup@v6 | |
| with: | |
| cache: true | |
| - run: mise run init --no-submodules | |
| - name: Resolve version | |
| id: meta | |
| run: | | |
| version="$(node -p "require('./apps/zotero/package.json').version")" | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| echo "tag=zt-$version" >> "$GITHUB_OUTPUT" | |
| echo "xpi=apps/zotero/dist/zotlit-zotero-$version.xpi" >> "$GITHUB_OUTPUT" | |
| # The changelog is keyed on the Obsidian plugin version (the headline | |
| # product); this companion release points at that same entry. | |
| echo "changelog=$(node -p "require('./apps/obsidian/package.json').version")" >> "$GITHUB_OUTPUT" | |
| prerelease="$(pnpm --filter @zotlit/scripts exec node -p \ | |
| "require('semver').prerelease('$version') !== null")" | |
| echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT" | |
| - name: Check release exists | |
| id: gate | |
| run: | | |
| if gh release view "${{ steps.meta.outputs.tag }}" >/dev/null 2>&1; then | |
| echo "exists=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "exists=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Build | |
| if: steps.gate.outputs.exists == 'false' | |
| run: pnpm turbo run build --filter=@zotlit/zotero | |
| # Requires a pre-existing `zotero-release` host with its manifests (bootstrap | |
| # manually — see docs/CI_SETUP.md). A missing host fails the step to | |
| # guard against the version-gate clobbering a newer beta. Runs after | |
| # Build because turbo may restore dist/** from cache, dropping | |
| # pre-existing files. | |
| - name: Download current update manifests | |
| if: steps.gate.outputs.exists == 'false' | |
| run: gh release download zotero-release --pattern 'update*.json' --dir apps/zotero/dist | |
| # Replaces a channel entry only when this version is gt the existing one. | |
| # Emits `attest` scoped to files this run actually built. | |
| - name: Build update manifests | |
| id: updates | |
| if: steps.gate.outputs.exists == 'false' | |
| run: node apps/zotero/scripts/build-update-json.ts | |
| # `--latest=false`: Zotero ships via update.json, not the repo's "Latest" | |
| # release pointer, which is left to the Obsidian plugin (the headline | |
| # product) so a stable dual release resolves "Latest" deterministically. | |
| - name: Create release | |
| if: steps.gate.outputs.exists == 'false' | |
| run: | | |
| notes=$(printf '%s\n\n%s' \ | |
| "ZotLit Zotero companion v${{ steps.meta.outputs.version }}" \ | |
| "Release notes: https://zotlit.aidenlx.site/changelog/${{ steps.meta.outputs.changelog }}") | |
| gh release create "${{ steps.meta.outputs.tag }}" \ | |
| --title "${{ steps.meta.outputs.tag }}" \ | |
| --target "${{ github.sha }}" \ | |
| --notes "$notes" \ | |
| ${{ steps.meta.outputs.prerelease == 'true' && '--prerelease' || '--latest=false' }} \ | |
| "${{ steps.meta.outputs.xpi }}" | |
| # The `zotero-release` tag is permanent: prerelease, never "Latest". Uses `edit` | |
| # to enforce the pre-existence invariant from the Download step. | |
| - name: Refresh update channel | |
| if: steps.gate.outputs.exists == 'false' | |
| run: | | |
| notes="apps/zotero/dist/release-host-notes.md" | |
| gh release edit zotero-release \ | |
| --title "Release Manifest" \ | |
| --notes-file "$notes" \ | |
| --prerelease \ | |
| --latest=false | |
| for f in apps/zotero/dist/update*.json; do | |
| gh release upload zotero-release "$f" --clobber | |
| done | |
| # Scoped to files this run actually built: the XPI and any channel | |
| # manifest the version-gate rewrote. | |
| - name: Attest provenance | |
| if: steps.gate.outputs.exists == 'false' | |
| uses: actions/attest@v4 | |
| with: | |
| subject-path: ${{ steps.updates.outputs.attest }} |