Skip to content

Merge pull request #707 from aidenlx/feat/pandoc-ref #97

Merge pull request #707 from aidenlx/feat/pandoc-ref

Merge pull request #707 from aidenlx/feat/pandoc-ref #97

Workflow file for this run

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 }}