Skip to content

ENG-2099 Test and fix Obsidian Linear release pipeline - #1311

Open
trangdoan982 wants to merge 12 commits into
mainfrom
eng-2099-test-obsidian-linear-release-pipeline
Open

ENG-2099 Test and fix Obsidian Linear release pipeline#1311
trangdoan982 wants to merge 12 commits into
mainfrom
eng-2099-test-obsidian-linear-release-pipeline

Conversation

@trangdoan982

@trangdoan982 trangdoan982 commented Aug 19, 2026

Copy link
Copy Markdown
Member

Obsidian release pipeline: test report

https://www.loom.com/share/ebfdaee0363449a784fb7030c3bb0d02

Six bugs found. Five are fixed in code. Last one about updating versioning on package.json need discussion.
Note for reviewer: Easier to read through Github comments to see the bug overview first, then come back here for more details about testing and fixing of each bug

Terms used below

  • publish repo: DiscourseGraphs/discourse-graph-obsidian, the public repo the Obsidian community store and BRAT read from
  • test repo: DiscourseGraphs/discourse-graph-obsidian-test, created for this testing
  • beta workflow: obsidian-main.yaml
  • stable workflow: obsidian-release.yaml

How the tests were set up

publish.ts hardcodes the publish repo in three constants. I temporarily pointed those constants and both workflows' credentials at the test repo. Those are the commits on #1311 marked TEMP, each carrying a TODO(ENG-2099): REVERT BEFORE MERGE marker.

I triggered everything from the branch with workflow_dispatch. That runs the branch's copy of the workflow and script, and still has access to repo secrets:

gh workflow run obsidian-main.yaml --repo DiscourseGraphs/discourse-graph \
  --ref eng-2099-test-obsidian-linear-release-pipeline

Bug A (fixed): betas published as official releases and overwrote the store's manifest

isExternalRelease() returned true for beta as well as stable.

Fixed in 2f7a0ce7. Only a bare x.y.z counts as external now, so betas and alphas stay pre-releases and leave main alone.

Verification

I checked the classification against the edited source across fourteen inputs: stable, -beta.N, bare -beta, -alpha-name, -rc.N, plus malformed ones like 1.5, v1.5.2, empty, and abc. All fourteen behaved as intended. Two pre-existing quirks are harmless: 1.5.2.1 and 01.5.2 pass validateVersion, because its regex is not anchored at the end, and then classify as pre-releases instead of being rejected.

Then proven in five beta runs. Every beta came out as prerelease=true:

The decisive check is what those five runs did not do. After all of them, the test repo's main still had one commit and one file:

gh api repos/DiscourseGraphs/discourse-graph-obsidian-test/commits
  -> 4b58ca94 Initial commit

gh api "repos/.../git/trees/main?recursive=1"
  -> README.md

No manifest.json, no Release v... commit. Before the fix, each of those runs would have overwritten the store's manifest. GitHub also reported no "latest" release while only betas existed, which is the correct signal that a beta is not standing in for a stable release.

I confirmed each release's attached manifest.json asset carried the matching version and the right plugin id, by pulling the asset bytes rather than trusting the log:

gh api repos/.../releases/assets/<id> -H "Accept: application/octet-stream"
  -> version = 1.5.2-beta.3, id = discourse-graphs   (all five matched their tag)

Bug B (fixed): every run on main failed at the Linear step

The workflows referenced secrets.OBSIDIAN_LINEAR_RELEASE_KEY. That secret does not exist. The real name is LINEAR_RELEASES_ACCESS_KEY_OBSIDIAN, so the input resolved to an empty string and the action exited:

LINEAR_ACCESS_KEY:
##[error]access_key input is required

This is why it worked for me and failed for everyone else. The name was correct on the eng-1767 branch, which is why it passed there on 24 May, and it changed when that merged. Outcomes since:

gh run list --workflow obsidian-main.yaml --limit 60 --json conclusion
  -> {"cancelled": 3, "failure": 6}

Zero successes, ever, on main.

The part worth internalising: that step runs last, after publish has already written to the publish repo. Every one of those red runs still changed production. A failed run never meant "nothing shipped". It meant "shipped, then failed to tell Linear".

Fixed in all three references in 7b33e2d2, and confirmed live. The key resolves and the step passes.

Bug C (fixed): the beta counter never incremented

gh release list ran without --repo, so it queried the monorepo. The monorepo has no releases and no tags. The lookup always came back empty, and the code fell through to -beta.1 every time.

Fixed in 5eb67213 by pointing the lookup at the publish repo. I verified the shell logic locally against both repos before spending a run on it, then confirmed in sequence. The runs after the fix produced beta.2, beta.3, beta.4, and beta.5, each incrementing correctly.

Bug D (fixed): source files silently dropped from the publish repo

shouldExclude matched patterns with relativePath.includes(), and turned glob patterns into unanchored regexes. Any file whose name merely contained an excluded word was dropped. "out" ate DatacoreCallout.tsx. *.log became /.*.log/ and ate nativeJsonFileDialogs.ts via "diaLOGs".

Fixed in 1bb10ac7. Patterns now match whole path segments, and globs are anchored to a single segment with regex metacharacters escaped.

Verification

real publish, run 32539711072, commit 77f5b0d7:

src file count:  101 local -> 101 published   (was 99)
PRESENT  src/components/DatacoreCallout.tsx
PRESENT  src/utils/nativeJsonFileDialogs.ts

I re-ran the secret check against the enlarged set of 113 published files. No .env, secret, .pem, or credential files. .env, .env.branch, .env.example, nested .env.local, *.pem, *.log, node_modules, dist, scripts, and .git all stay excluded. Names that would previously have been landmines (Logger.ts, Layout.tsx, distance.ts, environment.ts, buildIndex.ts) are correctly kept.

Bug E (fixed): the published package.json was one release behind, permanently

There are two copies of the version, written at different times. updateManifest only rewrote manifest.json in the staging directory. package.json was copied at its pre-bump value, and updateLocalVersion, which does bump it, runs after updateMainBranch has already pushed.

Observed on both repos:

test repo, right after publishing 9.9.9:  manifest.json = 9.9.9         package.json = 1.5.2
publish repo:                             manifest.json = 1.5.2-beta.1  package.json = 1.5.2

Low user impact, since Obsidian reads manifest.json. It mattered because the published repo contradicted itself, and anything reading package.json got the wrong answer.

Fixed in 07180a4d with an updateStagedPackageVersion step that runs alongside updateManifest, before the push. Verified on the same 9.9.10 publish:

manifest.json: 9.9.10
package.json:  9.9.10

Bug F: stable workflow can't run step "Commit version bump"

main is protected by a ruleset, not by classic branch protection. That is why it reads as protected but the protection endpoint returns 404:

gh api repos/DiscourseGraphs/discourse-graph/rulesets
  -> "Main Branch Protection"  target=branch  enforcement=active
     rules:  pull_request, non_fast_forward
     bypass: OrganizationAdmin only

github-actions[bot] is not a bypass actor, so the Commit version bump step's git push is rejected whenever the workflow runs from main. The step sits after publishing, so a real stable release would create the GitHub release, push the publish repo, then fail, and never reach the Linear steps.

Retrying does not help. updateMainBranch reports no changes, createGithubRelease skips the existing release, updateLocalVersion rewrites the same two files, and the push is rejected again. The Linear half of the pipeline was unreachable for that version, permanently.

Options:

  1. Comment out this code in c4a86fc5. We just don't care about updating versioning in monorepo, only care about versioning in discourse-graph-obsidian
  2. add permission to github-actions. But this introduce potential security risks.

The stable workflow, tested

I dispatched it twice, both times with a deliberately fake version. That kept it from colliding with anything real, and kept the Linear complete command away from the genuine 1.5.4 release:

gh workflow run obsidian-release.yaml --repo DiscourseGraphs/discourse-graph \
  --ref eng-2099-test-obsidian-linear-release-pipeline -f version=9.9.9

First run, 32537199051. All ten steps green, the first time this workflow has run since May. It produced release 9.9.9 with prerelease=false and all three assets, and GitHub promoted it to "latest". The contrast with the five betas is exactly what we want. The push landed commit 150d5286 with 111 files. This run exposed Bugs D and F.

Second run, 32539711072, verifying those two fixes. It produced release 9.9.10, again prerelease=false with all three assets and promoted to "latest", and commit 77f5b0d7 with 113 files. The two extra files are the ones Bug D had been eating.

On the security question: across all 113 published files there are no .env, secret, .pem, or credential files, and package.json had its scripts block stripped as intended.

Both Linear commands worked in both runs. sync found the commit range and synced. complete advanced the release to Released.

  • 9.9.9 to Linear release 6803609662b6
  • 9.9.10 to Linear release 7a5dc916-0ee6-4dca-a0fb-2b496d8b18fa, logging Found 4 matching commits between 8d68b05 and 2dc6982

Two things about Linear to flag:

  • The workflow input says the version "must match an existing Linear release version", but nothing validates that. sync created brand new releases for both 9.9.9 and 9.9.10, each named "New release", and complete marked them Released. A typo will invent a release rather than fail.
  • The log line issues [ENG-2099] did not translate into anything. The 9.9.9 release ended with issueCount: 0. Do not read that log line as proof that issue attribution works.

Separately, one beta run's Linear sync correctly did nothing:

Found 0 matching commits ... Skipping release creation.

The only commit in range touched .github/, which is outside include_paths. That is right, and it is good evidence the path filtering works. It also means the GitHub release and the Linear release can legitimately diverge while the step reports success.

Concurrency: fast merges skip betas

Three dispatches fired back to back produced one running, one cancelled, one queued. With cancel-in-progress: false, GitHub keeps only one pending run per concurrency group, so a third arrival kills the waiting second:

32535403999  pending
32535402204  cancelled     <- killed by the run behind it
32535400477  in_progress

The two survivors computed beta.4 and beta.5 correctly, so there is no race in the version lookup. The queue serialises properly.

This is the mechanism behind the three cancelled runs on main, and it means rapid merges do not each produce a beta. Not a bug, but not obvious either. Worth deciding whether it is what we want.

Mine to finish:

  • Revert the four TEMP commits on ENG-2099 Test and fix Obsidian Linear release pipeline #1311 and take it out of draft.
  • Revert the two bot version bumps, 8776c607 for 9.9.9 and 2dc69828 for 9.9.10. They leave apps/obsidian/package.json and manifest.json at 9.9.10 on the branch. This must not merge carrying a 9.9.x version.
  • Clean up test artifacts:
    • delete the test repo
    • the two Linear "New release" entries for 9.9.9 and 9.9.10, the eight test repo releases, and the stale Linear release "New release" from 24 May that has no version and still sits In Progress.

@linear-code

linear-code Bot commented Aug 19, 2026

Copy link
Copy Markdown

ENG-2099

@supabase

supabase Bot commented Aug 19, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project zytfjzqyijgagqxrzbmz because there are no changes detected in packages/database/supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@vercel

vercel Bot commented Aug 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
discourse-graph Ready Ready Preview Aug 22, 2026 4:07am

Request Review

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

trangdoan982 and others added 2 commits August 19, 2026 10:46
isExternalRelease() returned true for beta as well as stable, and that one
boolean gates both the GitHub prerelease flag and the push to the publish
repo's main branch. A beta therefore shipped as a full release and
overwrote manifest.json on discourse-graph-obsidian's main branch, which is
what the Obsidian community store reads to pick a version (ENG-2106).

Only a bare x.y.z is external now, so betas and alphas stay pre-releases
and leave the publish repo's main branch alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The workflows referenced secrets.OBSIDIAN_LINEAR_RELEASE_KEY, which does
not exist. The repo secret is named LINEAR_RELEASES_ACCESS_KEY_OBSIDIAN, so
the input resolved to an empty string and every run died at the Linear sync
step with "access_key input is required".

The name was correct on eng-1767 (which is why it passed there on
2026-05-24) and was changed on merge. obsidian-main.yaml has never once
succeeded on main since: 6 failures, 3 cancelled, 0 successes.

Note the sync step runs last, after publish has already written to the
publish repo, so these red runs were still mutating production.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Do not merge while this is in place. Retargets publish.ts at the private
discourse-graph-obsidian-test repo so the full pipeline (GitHub release,
manifest push, Linear sync) can be exercised from this branch via
workflow_dispatch without touching the real publish repo.

Running from a branch isolates which code runs, not where it writes, so the
target constant is what actually sandboxes the test.

Revert before merge — see TODO(ENG-2099) in scripts/publish.ts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@trangdoan982
trangdoan982 marked this pull request as draft August 19, 2026 14:52
Do not merge while this is in place. publish.ts reads its credential from the
OBSIDIAN_PLUGIN_REPO_TOKEN env var, and the workflow is what maps a secret
onto it, so repointing the env var is enough to keep a test run off the real
publish repo without touching the script.

Pairs with the temporary test-repo target in scripts/publish.ts. Requires a
repo secret named OBSIDIAN_TEST_REPO_TOKEN, scoped to
discourse-graph-obsidian-test with Contents: read and write.

Revert before merge — see TODO(ENG-2099) in the workflow env block.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gh release list had no --repo, so it queried the monorepo, which has zero
releases and zero tags. LATEST_N was therefore always empty and the else
branch always won, recomputing -beta.1 forever. Confirmed in run
32530653062: "next beta: 1.5.2-beta.1" followed by "already exists,
skipping creation" and then "Publication completed successfully" — a green
run that shipped nothing.

Point the lookup at the publish repo, where releases actually live. The
lookup also needs a credential that can read the target, since github.token
cannot see a private repo; reuse whatever publish.ts is configured with so
this works for both the real public repo and the private test target.

Also drops the sed that stripped a -beta suffix from package.json. After the
Bug A fix updateLocalVersion only runs for stable releases, so package.json
only ever holds a bare x.y.z.

Verified locally against both repos: latest_n=1 -> next=1.5.2-beta.2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Do not merge while this is in place. Same treatment as obsidian-main.yaml so
the stable release path can be exercised against the throwaway test repo.

Revert before merge — see TODO(ENG-2099) in the workflow env block.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
trangdoan982 and others added 2 commits August 21, 2026 20:14
shouldExclude tested patterns with relativePath.includes(), and turned glob
patterns into unanchored regexes, so any file whose name merely contained an
excluded word was silently dropped from the publish repo. "out" ate
src/components/DatacoreCallout.tsx, and "*.log" becoming /.*.log/ ate
src/utils/nativeJsonFileDialogs.ts via "diaLOGs". Confirmed on a real
publish: 101 local src files, 99 published.

Match whole path segments instead, and anchor globs to a single segment with
regex metacharacters escaped.

Verified against the full apps/obsidian tree: exactly those two files flip
from excluded to included and nothing flips the other way. .env, .env.branch,
.env.example, nested .env.local, *.pem, *.log, node_modules, dist, scripts
and .git remain excluded; Logger.ts, Layout.tsx, distance.ts, environment.ts
and buildIndex.ts are correctly kept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
updateManifest only rewrote manifest.json in the staging directory.
package.json was copied at its pre-bump value, and updateLocalVersion — which
bumps it — runs after updateMainBranch has already pushed. The published
package.json was therefore always one release behind, permanently.

Observed on both repos: the test repo showed manifest.json 9.9.9 alongside
package.json 1.5.2 right after publishing 9.9.9, and the production repo
shows manifest.json 1.5.2-beta.1 alongside package.json 1.5.2.

Cosmetic for users, since Obsidian reads manifest.json, but the published
repo was internally inconsistent and anything reading package.json got the
wrong answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…kflow

The Commit version bump step can never succeed on main. The "Main Branch
Protection" ruleset requires a pull request on the default branch and does
not list github-actions[bot] as a bypass actor, so git push is rejected.

Because the step sits after publishing, a real stable release would create
the GitHub release and push the publish repo, then fail here and never reach
the Linear sync/complete steps. Retrying does not help: updateMainBranch
reports no changes, createGithubRelease skips the existing release,
updateLocalVersion rewrites the same files, and the push is rejected again.
The Linear half of the pipeline was unreachable for that version.

The step's own guard gives us the way out. Bumping package.json and
manifest.json by hand in the release PR means publish.ts writes values that
already match, git diff --quiet passes, and nothing needs pushing from CI —
so the remaining steps run. Commented out rather than deleted so the intent
and the re-enable conditions stay visible.

This also explains why the workflow's two green runs on 2026-05-24 proved
nothing: they ran on a branch, where git push targets the branch, not main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@trangdoan982 trangdoan982 changed the title ENG-2099 Test Obsidian Linear release pipeline ENG-2099 Test and fix Obsidian Linear release pipeline Aug 22, 2026
Comments that narrated what this PR fixes belong on the PR, not in the
source. Removed the bug references and the specific file names that were
dropped, and kept only what a future reader needs without this PR for
context:

- the community store reading manifest.json from the publish repo's main
  branch, which is why only finished releases are external
- the segment-matching contract for exclude patterns
- the ordering constraint that makes updateStagedPackageVersion necessary
- releases existing only in the publish repo, which is why the lookup needs
  an explicit --repo
- why the version-bump step is disabled and how to re-enable it

TEMP revert markers reduced to a single line each.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@@ -215,23 +216,14 @@ const validateVersion = (version: string): void => {
};

const isExternalRelease = (version: string): boolean => {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug A: -beta versions were published as official releases

This predicate returned true for beta as well as stable, and one boolean gated two separate decisions: the GitHub prerelease flag, and whether to push manifest.json to the publish repo's main branch. So a beta shipped as a full release, and it overwrote the manifest the Obsidian community store reads to pick a version. That is ENG-2106.

The publish repo's main still sits at 1.5.2-beta.1 today, with five Release v1.5.2-beta.1 commits, two of them landing after stable 1.5.3 shipped on 7 Aug.

Verified across fourteen version inputs, then in five beta runs: every release came out prerelease=true, and the test repo's main kept exactly one commit and one file throughout.

uses: linear/linear-release-action@v0
with:
access_key: ${{ secrets.OBSIDIAN_LINEAR_RELEASE_KEY }}
access_key: ${{ secrets.LINEAR_RELEASES_ACCESS_KEY_OBSIDIAN }}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug B: every run on main failed at the Linear step

This referenced secrets.OBSIDIAN_LINEAR_RELEASE_KEY, which does not exist. The real secret is LINEAR_RELEASES_ACCESS_KEY_OBSIDIAN, so the input resolved to an empty string and the action exited with access_key input is required.

The name was correct on eng-1767, which is why it passed there on 24 May, and it changed on merge. Since then: 6 failures, 3 cancelled, 0 successes.

Worth noting the step runs last, after publish has already written to the publish repo. Those red runs still changed production.

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' \
STABLE_VERSION=$(node -p "require('./apps/obsidian/package.json').version")
LATEST_N=$(gh release list --repo "$PUBLISH_REPO" --limit 100 --json tagName --jq '.[].tagName' \

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug C: the beta counter never incremented

gh release list ran without --repo, so it queried the monorepo, which has 0 releases and 0 tags. The lookup always came back empty and the code fell through to -beta.1 every time.

The failure mode was the quiet kind: the recomputed version already existed, createGithubRelease took its early return, and the run exited green having shipped nothing.

After the fix the runs produced beta.2 through beta.5, incrementing correctly.


// 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 => {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug D: source files were silently dropped from the publish repo

The old matcher used relativePath.includes(pattern) and turned globs into unanchored regexes, so any file whose name merely contained an excluded word was dropped. "out" ate src/components/DatacoreCallout.tsx. *.log became /.*.log/ and ate src/utils/nativeJsonFileDialogs.ts via "diaLOGs".

Confirmed on a real publish: 101 local source files, 99 published. After the fix, 101 of 101.

Before committing I diffed old against new across the whole apps/obsidian tree: exactly those two files flip to included, nothing flips the other way. Re-checked secrets against the enlarged 113-file set, since loosening an exclusion filter on a public repo is where a leak would hide. Nothing sensitive published.


// 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 => {

@trangdoan982 trangdoan982 Aug 22, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug E: the published package.json was permanently one release behind

updateManifest only rewrote manifest.json in the staging directory. package.json was copied at its pre-bump value, and updateLocalVersion runs after updateMainBranch has already pushed.

Result on both repos:

test repo, right after publishing 9.9.9:  manifest.json = 9.9.9         package.json = 1.5.2
publish repo:                             manifest.json = 1.5.2-beta.1  package.json = 1.5.2

Low user impact, since Obsidian reads manifest.json, but the published repo contradicted itself. Verified on the 9.9.10 publish: both files now read 9.9.10.

git commit -m "chore: release obsidian ${VERSION} [skip ci]"
git push
fi
# Disabled: the default-branch ruleset requires a pull request and does not

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug E: this step could never succeed on main

The Main Branch Protection ruleset requires a pull request on the default branch and does not list github-actions[bot] as a bypass actor, so the push is always rejected there.

The step sits after publishing, so a real stable release would create the GitHub release, push the publish repo, then fail here and never reach the Linear steps. Retrying does not help: updateMainBranch reports no changes, createGithubRelease skips the existing release, updateLocalVersion rewrites the same files, and the push is rejected again. The Linear half of the pipeline was unreachable for that version.

The two green runs on 24 May prove nothing. They ran on a branch, where git push targets the branch instead of main.

The step's own git diff --quiet guard is what makes disabling it safe: bump the version in the release PR and there is nothing for CI to push. I did not reproduce the rejection, since that needs a real stable release from main.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant