{"slug": "i-shipped-a-plugin-with-one-branch-i-hadn-t-tested-so-i-built-a-fallback", "title": "I shipped a plugin with one branch I hadn't tested — so I built a fallback", "summary": "A non-engineer product lead used Claude Code to build an Obsidian plugin called Publora, which schedules sending notes to social accounts, and documented the pitfalls of getting it through the Obsidian catalog. The developer encountered review guideline issues like avoiding innerHTML and using requestUrl, submission portal errors related to GitHub organization ownership and public membership, and rate limiting. They also implemented GitHub artifact attestation in a release workflow to ensure the built main.js matches the source.", "body_md": "I wrote the Obsidian plugin with Claude Code: it typed the code; the rules, reviews, and decisions were mine. I led, argued with it, redid things, and submitted the plugin for review myself. I'm not an engineer. My job is getting people to actually use our product.\n\nThe plugin itself is simple: one `main.js`\n\n, 557 lines, zero dependencies, [Obsidian API](https://docs.obsidian.md/Reference/TypeScript+API/Plugin) only. It takes the note you have open and sends it to your social accounts on a schedule.\n\nMost of the evening, though, went into getting it through the Obsidian catalog. Here's what I tripped over, so hopefully you don't have to.\n\nBefore a plugin lands in the catalog, it gets reviewed against the [Plugin guidelines](https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines). I read them beforehand and still came back with notes. Some requirements are much easier to notice once a reviewer points directly at them.\n\nThese were my six:\n\n`innerHTML`\n\n.`createEl`\n\n. They check for this because `innerHTML`\n\nplus third-party text is an open door.`styles.css`\n\n,`titleEl`\n\n,`Publora: Publora: Send note`\n\n. It stutters.`requestUrl`\n\n`fetch`\n\nruns into problems on mobile, so if the plugin is `isDesktopOnly: false`\n\n, use Obsidian's API.None of this changed what the plugin did. But doing it upfront would have saved me a review round and a couple of days.\n\nSubmission goes through [community.obsidian.md](https://community.obsidian.md/): sign in with your Obsidian account, then connect GitHub separately. The happy path is documented. The potholes aren't.\n\nMine:\n\n**\"You do not own this repository\" — when you clearly have access.** If the repo belongs to an organization, choose the organization as the submission owner instead of `Myself`\n\n. The error disappeared immediately.\n\n**Your GitHub organization membership has to be public.** The portal only sees public members. None of ours were public, so as far as the portal was concerned, our organization contained approximately nobody.\n\nOne request fixes your membership:\n\n```\nPUT /orgs/{org}/public_members/{username}\n```\n\nThe catch: you can only make your *own* membership public this way. [Each person has to do it with their own token.](https://docs.github.com/en/rest/orgs/members#set-public-organization-membership-for-the-authenticated-user)\n\n**The rate limiter.** After several attempts, the portal starts replying with `Please wait before trying again`\n\n. It doesn't say how long, and clicking again only makes things worse.\n\nThe solution turned out to be extremely technical: leave it alone and come back later. Then click once.\n\nThe review passed, but there was one note left: the release files had no artifact attestation.\n\nIf you haven't run into this before, the problem is pretty straightforward. Users don't install the source code they're looking at in your GitHub repository. They install the built `main.js`\n\nattached to a GitHub Release.\n\nThose two files *can* be different.\n\n[Artifact attestation](https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations/using-artifact-attestations-to-establish-provenance-for-builds) lets someone verify where the release file came from. GitHub Actions, through [Sigstore](https://www.sigstore.dev/), ties the artifact to a specific commit and workflow. This is the `release.yml`\n\nI ended up with — it also checks the tag matches the manifest version before it publishes:\n\n```\nname: Release\n\non:\n  push:\n    tags:\n      - '*'\n\npermissions:\n  contents: write\n  id-token: write\n  attestations: write\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Check the tag matches the manifest\n        run: |\n          tag=\"${GITHUB_REF_NAME}\"\n          manifest=\"$(node -p \"require('./manifest.json').version\")\"\n          if [ \"$tag\" != \"$manifest\" ]; then\n            echo \"Tag $tag does not match manifest version $manifest\"\n            exit 1\n          fi\n\n      - name: Attest the release assets\n        uses: actions/attest-build-provenance@v2\n        with:\n          subject-path: |\n            main.js\n            manifest.json\n            styles.css\n\n      - name: Publish the release\n        env:\n          GH_TOKEN: ${{ github.token }}\n        run: |\n          gh release create \"${GITHUB_REF_NAME}\" \\\n            main.js manifest.json styles.css \\\n            --title \"${GITHUB_REF_NAME}\" \\\n            --generate-notes\n```\n\nThe part that cost me the most time was these two permissions:\n\n```\nid-token: write\nattestations: write\n```\n\nWithout them, my workflow went green but no attestation appeared.\n\nEverything looked fine. Everything was not fine.\n\nOnce I added the permissions, the check actually passed and `verified GitHub artifact attestation`\n\nshowed up in the review.\n\nThis is probably my favorite part of the whole process: nobody has to take your word for it. The release itself carries proof of where it came from.\n\nAt submission time, I still had one thing I hadn't managed to verify.\n\nLogin in the plugin goes through OAuth, so the user gets a token. Our REST API, meanwhile, had historically been used with an API key.\n\nWould the API accept the token from this new OAuth flow?\n\nProbably.\n\nHad I actually tested it?\n\nNo.\n\nI could ship it and find out from the first person whose button stopped working. People do this more often than conference talks would have you believe.\n\nInstead, I built a fallback. The plugin sends the credential — whether that's the signed-in token or a pasted key — in the same `x-publora-key`\n\nheader. If the request comes back `401`\n\nand the token was the one that failed, it retries once with the API key from settings instead of stranding the user mid-post. If there's no key to fall back to, it says so in plain words rather than leaving a dead button:\n\n``` js\nasync function callApi(settings, method, path, body, plugin) {\n  const credential = plugin ? await plugin.credential() : settings.apiKey;\n  if (!credential) {\n    throw new Error('Not connected yet. Open Settings, then Publora, and press Connect.');\n  }\n\n  const usedToken = Boolean(\n    plugin && plugin.settings.oauth &&\n    credential === plugin.settings.oauth.accessToken\n  );\n\n  const response = await requestUrl({\n    url: BASE_URL + path,\n    method,\n    headers: { 'x-publora-key': credential, 'Content-Type': 'application/json' },\n    body: body ? JSON.stringify(body) : undefined,\n    throw: false,\n  });\n\n  if (response.status === 401) {\n    // The signed-in token was refused. If a key is also configured, use it\n    // rather than stranding the user mid-post.\n    if (usedToken && settings.apiKey) {\n      return callApi(Object.assign({}, settings, { oauth: null }), method, path, body, null);\n    }\n    throw new Error(\n      usedToken\n        ? 'Publora refused the signed-in account. Reconnect in Settings → Publora, or paste an API key there under Advanced.'\n        : 'Publora rejected the API key. Check it in Settings → Publora.',\n    );\n  }\n\n  return response.json;\n}\n```\n\nThe retry is just the same function calling itself with `oauth`\n\nnulled out, so it falls through to the key. A week later, working on OAuth for another add-on, I finally got to test the real thing: the REST API accepts the token fine, and the fallback never fired.\n\nBut I didn't know that when I submitted the plugin.\n\nThat's the bit I want to keep from this whole exercise. Sometimes you have a branch you can't test before release. You don't have to pretend otherwise. If the failure mode is predictable and the fallback is cheap, you can put the uncertainty into the program instead of handing it to the user.\n\nAs of August 19: 33 installs.\n\nNot a lot.\n\nBut the version distribution shows most of those installs on the latest version, so people are updating rather than installing once and disappearing.\n\nFor comparison, our extension on another marketplace shows 772 \"downloads.\" That counter also eats mirrors and editor caches. At one point it jumped by 26 in half an hour when basically nobody knew the extension existed.\n\nSo right now I'll take the 33.\n\nAt least I know what they mean.\n\nI built this plugin with AI, and I'm not particularly interested in hiding that. Claude Code typed most of it. I decided what it should do, reviewed what it produced, dealt with the submission, and decided what to do when I couldn't verify something before release.\n\nThe typing is increasingly the easy part.\n\nThe annoying part is still noticing the thing you haven't tested.\n\nWhat do you do when you reach release with one branch still uncertain: ship it and wait for the bug report, or build the fallback first?", "url": "https://wpnews.pro/news/i-shipped-a-plugin-with-one-branch-i-hadn-t-tested-so-i-built-a-fallback", "canonical_source": "https://dev.to/eugeniya_ivanova_4a58eadc/i-shipped-a-plugin-with-one-branch-i-hadnt-tested-so-i-built-a-fallback-lc8", "published_at": "2026-08-20 08:30:58+00:00", "updated_at": "2026-08-20 08:44:35.742233+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools"], "entities": ["Obsidian", "Claude Code", "Publora", "GitHub", "Sigstore", "GitHub Actions"], "alternates": {"html": "https://wpnews.pro/news/i-shipped-a-plugin-with-one-branch-i-hadn-t-tested-so-i-built-a-fallback", "markdown": "https://wpnews.pro/news/i-shipped-a-plugin-with-one-branch-i-hadn-t-tested-so-i-built-a-fallback.md", "text": "https://wpnews.pro/news/i-shipped-a-plugin-with-one-branch-i-hadn-t-tested-so-i-built-a-fallback.txt", "jsonld": "https://wpnews.pro/news/i-shipped-a-plugin-with-one-branch-i-hadn-t-tested-so-i-built-a-fallback.jsonld"}}