Skip to content

chore(release): adopt shared forensic release pipeline#289

Merged
chhoumann merged 2 commits into
masterfrom
chore/shared-release-pipeline
Jul 11, 2026
Merged

chore(release): adopt shared forensic release pipeline#289
chhoumann merged 2 commits into
masterfrom
chore/shared-release-pipeline

Conversation

@chhoumann

@chhoumann chhoumann commented Jul 11, 2026

Copy link
Copy Markdown
Owner

What and why

Migrates PodNotes onto the shared forensic release pipeline in
chhoumann/obsidian-plugin-workflows
as its first, parity-proof consumer. PodNotes' own machine-PR release flow was
the model for that shared repo, so this is a "fold the reference implementation
back into the library" migration: a fix now lands once in the shared repo and
every plugin picks it up by bumping a pin.

Christian's decision this encodes:

  • PR-to-release is the standard. Merging the standing machine-generated
    release PR is the sole release act. No auto-release, no manual dispatch on the
    happy path.
  • Full forensics. Every guarantee that made PodNotes' release path
    tamper-evident is preserved in the shared reusables (see the parity table).
  • Per-repo App bots. The release commit and PR are authored by a per-repo
    GitHub App bot the maintainer provisions and controls, replacing the ambient
    github-actions[bot].

The four in-repo release workflows and the release toolkit scripts are replaced
by three thin caller stubs (trigger + uses:); all logic lives in the shared
reusables pinned at @v2 with workflows-ref: v2.

Line delta

Area Change
Workflows + scripts + package.json +89 / -3507 (net -3418) - 4 old workflows + 6 scripts replaced by 3 stubs (150 lines total)
package-lock.json +3031 / -8763 (net -5732) - pruned the semantic-release dependency tree

Parity audit

Every check in the old PodNotes workflows was read line by line and mapped to
its new home in the shared reusables. Fully preserved:

Guarantee (old PodNotes) New home (shared repo @v2)
Standing PR provenance (author=bot, base=master, head repo, release/<semver> branch, exact title) release-prepare.yml open-pr; re-checked in release-validate.yml + release.yml resolve
Exact version-file diff (only version fields moved, exact 4-file set for npm) release.yml build "Validate release commit and version-file diff"; release-contract.mjs validateVersionFiles
Commit-message contract + single parent release-validate.yml head-commit check; release.yml resolve
Squash-parent + tree-sha equality release-validate.yml; release.yml resolve
Prior-tag ancestry (previous tag exists, valid metadata, ancestor of base) release-validate.yml validate
Durable recovery branch release-run/<version> created in release-validate.yml, consumed/cleaned in release.yml publish
Attestation + post-publish asset re-hash release.yml attest + publish (attest-build-provenance, gh attestation verify, per-asset re-download + sha256)
Release-commit self-classification skip + "previous release is public" baseline release-prepare.yml "Classify current commit" + "Verify previous release is public"
Merge-subject validator release-validate.yml + release.yml resolve (exact release(version): Release <v> (#<PR>))
versions.json <-> manifest.minAppVersion floor mapping for the new entry + history-unchanged release-validate.yml compatibility block; release-contract.mjs validateVersionFiles / materializeVersionFiles

Documented deviations (intended)

  1. App-bot authorship. Old flow authored the release commit + PR as
    github-actions[bot]; the shared flow authors them as podnotes-release-bot[bot]
    via a GitHub App token minted in prepare. The GitHub release object is still
    created by GITHUB_TOKEN (author stays github-actions[bot]).
  2. Pre-merge CI dispatch dropped. The old open-pr step explicitly dispatched
    test.yml on the release branch. The shared flow relies on the App-authored
    PR triggering pull_request CI naturally (PodNotes' Test workflow runs on
    pull_request), so the explicit dispatch is unnecessary.

Additional deviations found and flagged (not silently dropped)

These are real differences between the old PodNotes internals and the shared
reusables. None block CI; they are behavioral notes for a follow-up on the
shared repo (the stubs carry no logic, so they cannot be fixed here):

  1. retryTransientGitHubRead is absent. The old release-trigger.yml
    wrapped every read-only GitHub API call in a bounded 5xx retry
    ([500,1500,4000]ms). The shared reusables have no equivalent generic
    read-retry wrapper. Impact: a transient GitHub 5xx during validation fails the
    run instead of retrying - resilience only, not a security guarantee.
  2. minAppVersion "stable-semver + must-increase" floor validation is weaker.
    The old compatibility contract asserted manifest.minAppVersion is a stable
    semver and monotonically increases, and compared versions.json history
    against the previous stable tag exactly. The shared reusables preserve the
    new-entry floor mapping and the base-vs-release history check, but only
    type-check minAppVersion as a non-empty string and compare history against the
    tested base (not the previous tag). Impact: defense-in-depth on the
    compatibility floor is reduced; the core "you can't rewrite released history
    or mismap the new floor" guarantee remains.
  3. Merge-subject (#PR) suffix is now mandatory (was optional in the old
    validator). GitHub squash-merge appends (#<PR>) by default, so this matches
    PodNotes' actual merge settings - stricter, not a loss. Confirm squash-merge
    stays enabled.

Test-file note

scripts/release-workflow-contract.test.ts was deleted (it was not in the
original migration list). It asserted the inline JavaScript of the old
release-trigger.yml / release.yml (merge-subject validator, tag-visibility
contract, compatibility transition, transient-retry). That inline code is
exactly what the thin stubs remove, so the test's subject no longer exists here.
Those behaviors are now covered by the shared repo's own scripts/*.test.mjs
(for the toolkit functions) and the parity table above (for the inline
workflow forensics).

Dry-run plan evidence

Ran the shared repo's release-plan.mjs against this clone's real git history
(node scripts/release-plan.mjs --cwd <podnotes> --package-manager npm):

Current HEAD (6ac0187, sole post-tag commit is refactor: remove unused capability kernel) - correctly declines a release:

{ "baseSha": "6ac0187...", "release": false, "notes": "", "schemaVersion": 1 }

Historical base dd50552 (the commit the real 2.19.2 released from) -
reproduces the actual release exactly:

{
  "baseSha": "dd50552...",
  "previousVersion": "2.19.1",
  "nextVersion": "2.19.2",
  "releaseType": "patch",
  "release": true,
  "notes": "## [2.19.2](.../compare/2.19.1...2.19.2) ...\n### Bug Fixes\n* **orca:** invoke the runner via the worktree-local bin ... (#285) ..."
}

Gates (all green locally)

npm run lint, npm run format:check (oxfmt reformatted the copied stubs to
PodNotes' 2-space style), npm run typecheck, npm run test
(1048 tests / 72 files, deleted script tests dropped out cleanly),
npm run build. The three stubs pass actionlint and YAML parse.

REQUIRED BEFORE MERGE - GitHub App setup

The pipeline fails closed without a per-repo release-bot App. Before the next
release, Christian must (steps in the shared repo README, "Per-repo GitHub App
setup"):

  1. Create a GitHub App named e.g. PodNotes Release Bot with slug
    podnotes-release-bot (must match release-bot-app-slug). Repository
    permissions: Contents R/W, Pull requests R/W. No account perms, no webhook.
  2. Generate a private key; install the App on this repo only.
  3. Add repo variable RELEASE_APP_ID (numeric App id) and secret
    RELEASE_APP_PRIVATE_KEY (the full .pem).
  4. If master is protected, add the App as an allowed bypass/merge actor per the
    ruleset (the maintainer still merges; the App only authors).

CI is unaffected meanwhile (Test, docs, CodeQL etc. keep running). This PR
deletes the old release flow, so releases are blocked until the App is set up -
nothing else breaks. After setup, smoke-test via Actions -> Prepare release ->
Run workflow with the dry-run: true line uncommented before the first real
release.

Do not auto-merge

Christian merges PodNotes himself after the App is provisioned.


Update while landing: shared workflows are now pinned to the exact v3 SHA (68068d2) rather than floating @v2. v3 = v2-parity + pending-minAppVersion floors (upstream #3, fixing the confirmed Codex finding here) + no more releases for chore commits (upstream #4). Both upstream PRs were adversarially reviewed and are covered by the new upstream self-CI.

Replace PodNotes' four in-repo release workflows with three thin caller
stubs that consume the reusable pipeline in
chhoumann/obsidian-plugin-workflows (@v2). The release-plan and
release-contract scripts (and their tests) moved into that shared repo;
this repo now carries only trigger + uses: stubs.

- release-prepare.yml: workflow_run(Test) + workflow_dispatch escape hatch
- release-trigger.yml: pull_request_target(closed), no workflows-ref
- release.yml: workflow_dispatch, npm assets + docs build verify commands
- delete scripts/{release-plan,release-contract}.mjs + tests,
  semantic-release-plugins.d.ts, and release-workflow-contract.test.ts
  (its subject, the inline workflow JS, is removed)
- drop the release:plan script and the now-unused semantic-release
  devDependencies (they live in the shared repo)

Releases are blocked until the podnotes-release-bot GitHub App is created
and RELEASE_APP_ID / RELEASE_APP_PRIVATE_KEY are set; CI is unaffected.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 11, 2026

Copy link
Copy Markdown

Deploying podnotes with  Cloudflare Pages  Cloudflare Pages

Latest commit: a12a2d0
Status: ✅  Deploy successful!
Preview URL: https://f1ed0ea5.podnotes.pages.dev
Branch Preview URL: https://chore-shared-release-pipelin.podnotes.pages.dev

View logs

@chhoumann

Copy link
Copy Markdown
Owner Author

Ready for your merge.

All checks are green (Test, CodeQL x2, Analyze actions, Cloudflare Pages) and no review threads are outstanding. I am not merging - this is yours to merge per the PR-to-release standard.

One dependency before the next release: this PR deletes the old release flow, so releases are blocked until the podnotes-release-bot GitHub App is created and RELEASE_APP_ID (var) + RELEASE_APP_PRIVATE_KEY (secret) are set (steps in the PR body / shared repo README). CI and everything else keep working meanwhile - only releasing is gated. After the App is provisioned, smoke-test via Actions -> Prepare release -> Run workflow with the dry-run: true line uncommented, then do the first real release by merging the standing release PR the bot opens.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb2c16b3eb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/release-prepare.yml Outdated
Comment thread .github/workflows/release-trigger.yml Outdated
chhoumann added a commit to chhoumann/obsidian-plugin-workflows that referenced this pull request Jul 11, 2026
A commit on the default branch may raise manifest.minAppVersion before the
release that materializes it into versions.json. The contract previously
required strict equality between the current version's recorded floor and
the manifest floor, so any floor bump landed on the default branch broke
every subsequent Prepare and validate run until hand-edited.

Port the pending-floor semantics from PodNotes' local pipeline (its 2.18.4
fix): the manifest floor must equal the recorded floor or increase from it,
never fall below it. Tag-state checks stay strict because a tag always
materializes its floor. Found by Codex review on chhoumann/PodNotes#289.
Pins the three reusable-workflow refs and both workflows-ref inputs to
the exact obsidian-plugin-workflows commit (v3: pending-floor support,
no chore releases, self-CI) instead of the floating v2 tag. Every
pipeline stage resolves the same toolkit commit, closing the mid-flight
tag-move race found in adversarial review, and matching the repo's
SHA-pinning posture for all other actions.
@chhoumann chhoumann merged commit d556f31 into master Jul 11, 2026
6 checks passed
@chhoumann chhoumann deleted the chore/shared-release-pipeline branch July 11, 2026 10:52
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