diff --git a/.github/workflows/obsidian-main.yaml b/.github/workflows/obsidian-main.yaml index 84ff344c6..568d4b600 100644 --- a/.github/workflows/obsidian-main.yaml +++ b/.github/workflows/obsidian-main.yaml @@ -18,6 +18,10 @@ concurrency: env: OBSIDIAN_PLUGIN_REPO_TOKEN: ${{ secrets.OBSIDIAN_PLUGIN_REPO_TOKEN }} + PUBLISH_REPO: DiscourseGraphs/discourse-graph-obsidian + # Inlined into the bundle at build time by scripts/compile.ts. Without it the + # plugin falls back to a dev URL, so released builds call localhost. + NEXT_API_ROOT: https://discoursegraphs.com/api SUPABASE_URL: ${{ secrets.SUPABASE_URL }} SUPABASE_PUBLISHABLE_KEY: ${{ secrets.SUPABASE_PUBLISHABLE_KEY }} GH_TOKEN: ${{ github.token }} @@ -48,17 +52,20 @@ jobs: - name: Compute next beta version id: version + env: + # Releases exist only in the publish repo, never in the monorepo, so the + # lookup needs an explicit --repo and a token that can read that repo. + GH_TOKEN: ${{ env.OBSIDIAN_PLUGIN_REPO_TOKEN }} run: | - STABLE_VERSION=$(node -p "require('./apps/obsidian/package.json').version" | sed 's/-beta\.[0-9]*//') - LATEST_N=$(gh release list --limit 50 --json tagName --jq '.[].tagName' \ + # manifest.json is the plugin's canonical version: it is what Obsidian + # and the community store read. package.json is private and its version + # is incidental. + STABLE_VERSION=$(node -p "require('./apps/obsidian/manifest.json').version") + LATEST_N=$(gh release list --repo "$PUBLISH_REPO" --limit 100 --json tagName --jq '.[].tagName' \ | grep "^${STABLE_VERSION}-beta\." \ | sed "s/^${STABLE_VERSION}-beta\.//" \ | sort -n | tail -1) - if [ -n "$LATEST_N" ]; then - NEXT_BETA="${STABLE_VERSION}-beta.$((LATEST_N + 1))" - else - NEXT_BETA="${STABLE_VERSION}-beta.1" - fi + NEXT_BETA="${STABLE_VERSION}-beta.$(( ${LATEST_N:-0} + 1 ))" echo "Stable version: $STABLE_VERSION, next beta: $NEXT_BETA" echo "version=$NEXT_BETA" >> "$GITHUB_OUTPUT" @@ -68,5 +75,5 @@ jobs: - name: Sync Linear release uses: linear/linear-release-action@v0 with: - access_key: ${{ secrets.OBSIDIAN_LINEAR_RELEASE_KEY }} + access_key: ${{ secrets.LINEAR_RELEASES_ACCESS_KEY_OBSIDIAN }} include_paths: "apps/obsidian/**,packages/database/**,packages/utils/**" diff --git a/.github/workflows/obsidian-release.yaml b/.github/workflows/obsidian-release.yaml index d4debb51e..9a3ed530c 100644 --- a/.github/workflows/obsidian-release.yaml +++ b/.github/workflows/obsidian-release.yaml @@ -13,6 +13,9 @@ permissions: env: VERSION: ${{ inputs.version }} OBSIDIAN_PLUGIN_REPO_TOKEN: ${{ secrets.OBSIDIAN_PLUGIN_REPO_TOKEN }} + # Inlined into the bundle at build time by scripts/compile.ts. Without it the + # plugin falls back to a dev URL, so released builds call localhost. + NEXT_API_ROOT: https://discoursegraphs.com/api SUPABASE_URL: ${{ secrets.SUPABASE_URL }} SUPABASE_PUBLISHABLE_KEY: ${{ secrets.SUPABASE_PUBLISHABLE_KEY }} @@ -50,20 +53,15 @@ jobs: - name: Publish stable release run: cd apps/obsidian && npx tsx scripts/publish.ts --version "$VERSION" - - name: Commit version bump - run: | - if ! git diff --quiet -- apps/obsidian/package.json apps/obsidian/manifest.json; then - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git add apps/obsidian/package.json apps/obsidian/manifest.json - git commit -m "chore: release obsidian ${VERSION} [skip ci]" - git push - fi + # There is deliberately no version-bump commit here. The default-branch + # ruleset requires a pull request and grants github-actions[bot] no bypass, + # so a push from CI is always rejected on main. Bump apps/obsidian/ + # manifest.json and package.json in the release PR before dispatching. - name: Sync Linear release uses: linear/linear-release-action@v0 with: - access_key: ${{ secrets.OBSIDIAN_LINEAR_RELEASE_KEY }} + access_key: ${{ secrets.LINEAR_RELEASES_ACCESS_KEY_OBSIDIAN }} command: sync version: ${{ env.VERSION }} include_paths: "apps/obsidian/**,packages/database/**,packages/utils/**" @@ -71,6 +69,6 @@ jobs: - name: Complete Linear release uses: linear/linear-release-action@v0 with: - access_key: ${{ secrets.OBSIDIAN_LINEAR_RELEASE_KEY }} + access_key: ${{ secrets.LINEAR_RELEASES_ACCESS_KEY_OBSIDIAN }} command: complete version: ${{ env.VERSION }} diff --git a/apps/obsidian/scripts/publish.ts b/apps/obsidian/scripts/publish.ts index 7ea88d1a0..7f5dae16a 100644 --- a/apps/obsidian/scripts/publish.ts +++ b/apps/obsidian/scripts/publish.ts @@ -215,23 +215,14 @@ const validateVersion = (version: string): void => { }; const isExternalRelease = (version: string): boolean => { - // External releases are: - // 1. Stable releases (x.y.z) - // 2. Beta releases (x.y.z-beta.n) - - // Stable release pattern (x.y.z) + // The Obsidian community store reads manifest.json from the publish repo's + // main branch, so only a finished release may be external. Everything else + // ships as a GitHub pre-release and leaves that branch untouched. const stablePattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/; if (stablePattern.test(version)) { return true; } - // Beta release pattern (x.y.z-beta.n) - const betaPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-beta(\.\d+)?$/; - if (betaPattern.test(version)) { - return true; - } - - // Everything else (including alpha releases) is internal return false; }; @@ -302,17 +293,23 @@ const execCommand = async ( } }; +// Patterns match a whole path segment, never a substring of one, and globs are +// anchored to a single segment. +const segmentMatchesPattern = (segment: string, pattern: string): boolean => { + if (!pattern.includes("*")) return segment === pattern; + + const escaped = pattern + .split("*") + .map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) + .join("[^/]*"); + return new RegExp(`^${escaped}$`).test(segment); +}; + const shouldExclude = (filePath: string, baseDir: string): boolean => { - const relativePath = path.relative(baseDir, filePath); - return EXCLUDE_PATTERNS.some((pattern) => { - if (pattern.includes("*")) { - const regex = new RegExp(pattern.replace(/\*/g, ".*")); - return regex.test(relativePath) || regex.test(path.basename(filePath)); - } - return ( - relativePath.includes(pattern) || path.basename(filePath) === pattern - ); - }); + const segments = path.relative(baseDir, filePath).split(path.sep); + return segments.some((segment) => + EXCLUDE_PATTERNS.some((pattern) => segmentMatchesPattern(segment, pattern)), + ); }; const copyDirectory = ({ @@ -402,6 +399,18 @@ const sanitizePackageJsonForMirror = (tempDir: string): void => { } }; +// updateLocalVersion runs after the publish-repo push, so the release version +// has to be written into the staged copy here as well. +const updateStagedPackageVersion = (tempDir: string, version: string): void => { + const packageJsonPath = path.join(tempDir, "package.json"); + if (!fs.existsSync(packageJsonPath)) return; + + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); + packageJson.version = version; + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); + log(`Updated staged package.json version to ${version}`); +}; + const updateLocalVersion = (obsidianDir: string, version: string): void => { const packageJsonPath = path.join(obsidianDir, "package.json"); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); @@ -741,6 +750,7 @@ const publish = async (config: PublishConfig): Promise => { if (isExternal) { updateManifest(tempDir, version); + updateStagedPackageVersion(tempDir, version); await updateMainBranch(tempDir, version); updateLocalVersion(obsidianDir, version); } else {