Skip to content

fix: rebase monorepo release pipeline onto main, fix bump/security issues (supersedes #25) - #28

Merged
nsheaps-oura merged 14 commits into
mainfrom
claude/pr-25-completion-ulo62c
Aug 3, 2026
Merged

nsheaps-oura merged 14 commits into
mainfrom
claude/pr-25-completion-ulo62c

Conversation

@nsheaps-oura

@nsheaps-oura nsheaps-oura commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

What this does

This picks up #25 (the release pipeline that builds a GitHub Release for each userscript) and gets it ready to merge. #25's description doesn't actually match its own final code anymore - it says the repo uses one shared version number, but a later commit on that same branch changed it so each userscript has its own version. It also says built files get committed to the repo, but another later commit removed that too. I read through the actual commits and code instead of trusting the description, found a few real problems, and fixed them here.

What changed

  • Fixed the merge conflict with main. Main had bumped oxlint to a newer version after this branch was created. Took main's version and fixed up yarn.lock by hand, since I don't have network access in this environment to regenerate it automatically.
  • Removed a leftover "version": "0.1.0" from the root package.json. It's not used for anything now that each package has its own version.
  • Fixed a bug in the auto-bump script. If someone bumped a package's version by hand without changing anything else, the script would skip it entirely instead of noticing the bump - so the new version would never actually get released. Fixed the order of checks so this works correctly, and tested both cases by hand.
  • Pinned an action to a specific commit instead of a branch name. Every other action in these workflows is pinned this way already. This particular one is the step that creates the token used to push to main and publish releases, so it's the one that most needed pinning.
  • Fixed a misleading line in the README. It said the new download link is how GreasyFork updates people's scripts. That's backwards - GreasyFork actually overwrites that link for scripts listed on its site, which is the whole reason this pipeline uses GitHub Releases directly instead. Only people who installed a script straight from GitHub (not from GreasyFork) benefit from the new link.
  • Turned on the test step in CI. It was written but never actually run, because one package's placeholder test script always failed on purpose. Gave every package without real tests a placeholder that passes instead of one that fails, and added a test step to the CI workflow. The one package with real tests still runs them.
  • Brought back packages/_template. An earlier commit on this branch deleted it, treating it as a duplicate of packages/template. It's still on main today, so I restored it as it was.

What I didn't do

  • I couldn't actually run yarn install, the build, or the tests in this environment - there's no network access to any package registry here. I checked that all the JSON, YAML, and shell files are at least syntactically valid, but CI running on GitHub is the first real test of all this.
  • There's still an open question from feat: monorepo release pipeline (release-it + GitHub Release assets) #25 about whether main's branch protection actually lets the automation bot push to it. If it doesn't, the release job will fail the same way an earlier version of this pipeline did. Not something I can verify from here.
  • packages/template and packages/_template are still both there with no explanation of which one to use. I just restored _template as asked - didn't try to resolve that.
  • Once this merges, feat: monorepo release pipeline (release-it + GitHub Release assets) #25 should be closed. I didn't do that myself since I wasn't asked to close someone else's PR.

Testing

  • Checked the auto-bump script's logic by hand with two test cases (a version bumped manually, and an unrelated change) - both now behave correctly
  • Checked that all the JSON and YAML files are still valid after my edits
  • Made sure there are no leftover merge conflict markers anywhere
  • CI passing on this PR (watching it now)
  • An actual release working once this is on main

🤖 Generated with Claude Code

nsheaps-oura and others added 12 commits July 27, 2026 12:23
Ignores PR #7's approach per direct instruction; replaces it with a
release-based publish pipeline instead of a bot-commit-only one.
PR #7's one fix not already on main (graphite-to-github-button's
TypeScript source restoration) landed separately via #23's merge, so
nothing from it is lost.

Versioning (fork 1, per team-lead's resolution): single repo-wide
release-it version/tag/changelog/GitHub-Release, matching the
convention used by every other nsheaps repo (dotfiles, git-wt,
claude-utils, op-exec, agents, ...) - researched live via
`gh search code`, an existing internal patterns doc, and direct
inspection of nsheaps/agents (the org's actual Nx/yarn monorepo).
No nsheaps repo has genuinely independent per-package release
tags/changelogs to copy, so this is the established pattern, not a
per-package one. Removes the unused, unconfigured @changesets/cli
scaffold (no .changeset/ dir ever existed) in favor of release-it.

Distribution (fork 2, per team-lead's resolution): skips GreasyFork's
own hosting/update infra (confirmed live via GreasyFork's own docs and
our existing listing that GreasyFork forcibly rewrites @downloadURL/
@updateURL for any script listed on its site, so pointing those at
GitHub directly only works for scripts NOT relying on GreasyFork's own
update mechanism). Both public packages' @downloadURL/@updateURL now
point at `.../releases/latest/download/<package>.user.js` - verified
live against a real nsheaps release (git-wt) that this URL shape
302-redirects to the actual latest-tagged asset. Existing GreasyFork
listings (509840, 509841) are untouched; this doesn't retroactively
affect users who already installed from there.

Per-package opt-in: `"greasyforkPublish": true` in a package's own
package.json controls whether its dist/script.user.js gets uploaded to
the GitHub Release. github-actions-grafana-jump does NOT set it - its
header already says it's intentionally internal-only, and this
preserves that.

Pipeline (.github/workflows/release.yaml, triggered on push to main):
build all packages -> commit regenerated dist/*.user.js if changed
(generalizes the same idea #58 built, redesigned as one job instead of
a separate workflow to avoid racing release-it's own commit+tag) ->
release-it --ci (no-ops if there are no releasable commits since the
last tag) -> if a release was actually cut, upload each opted-in
package's dist/script.user.js, renamed to <package>.user.js, to that
release.

ci.yml unchanged in spirit (yarn run build is still the required
per-PR/per-push correctness gate - a build/typecheck failure already
fails this check) - pinned actions/checkout and actions/setup-node to
hashes while touching this file, since they were on floating major-
version tags.

Formalized gaps found in the existing packages while verifying current
state per the task's own instruction:
- github-to-graphite-button never had the copy-and-strip-use-strict
  build step graphite-to-github-button already had, so it never
  produced a dist/script.user.js at all - added the same step.
- Both packages' tsconfig.json gained the same sourceMap:false +
  explanatory comments graphite-to-github-button already carried from
  earlier work, now that github-to-graphite-button needs the same
  reasoning documented.
- github-actions-grafana-jump's "types": ["node", "greasemonkey"]
  override (needed under this repo's moduleResolution: bundler
  setting) was silently relying on @types/node being present only as
  an accidental transitive dependency of the now-removed
  @changesets/cli - added @types/node as an explicit devDependency.
- Pinned a freshly-resolved transitive dependency (yoctocolors, pulled
  in by release-it) to the same 2.1.2 already vetted/used elsewhere in
  the org, after a fresh resolution pulled in 2.2.0 and got blocked by
  a local supply-chain safe-chain minimum-package-age check.

Verified live, not from memory:
- Full clean `nx run-many --target=build --all` succeeds for all 5
  packages under the current toolchain (TS 6.0.3, nx 23.0.2).
- Both public packages' dist/script.user.js start with the UserScript
  header (use-strict-stripping still works) and carry the new
  @downloadURL/@updateURL.
- oxlint passes (exit 0) for all 5 packages; github-actions-grafana-
  jump's 14 existing unit tests pass.
- `release-it --ci --dry-run` runs non-interactively end-to-end:
  correctly computes v0.1.0 (no existing tags), generates a real
  conventional-commits CHANGELOG.md grouped by type from actual repo
  history, and shows the exact git add/commit/tag/push sequence and
  GitHub Release URL it would execute - this is what release.yaml
  actually runs in CI (release:ci), so this is a genuine dry run of the
  real pipeline, not a hand-simulated approximation.

Known open risk, not resolved here (task #45): main's live `require-pr`
ruleset hasn't been confirmed to actually grant the automation app's
settings.yml-declared bypass yet. release.yaml's pushes (both the
dist-regen commit and release-it's own commit+tag) depend on that
bypass actually working; if it doesn't, this pipeline's pushes to main
will fail the same way regen-dist.yaml's did before checkout-as-app
was added. No branch/PR fallback is implemented for this workflow
(unlike the narrower one this supersedes) since a release run that
can't land on main isn't meaningfully retryable via a side branch.

Not done here, left for a human call per the task's own instruction:
formally closing PR #7 - it should be closed with an explanatory
comment once this PR is confirmed to actually supersede it, not
unilaterally before that's certain.
User's explicit call: dist/ should never be committed to the repo at
all - it's a purely transient build artifact from now on, attached
directly to GitHub Releases as its only distribution mechanism.

release.yaml changes:
- Removed the "stage regenerated userscripts if any changed" step
  (git add/commit/push of dist/*.user.js) entirely.
- Reordered: release-it now runs FIRST (deciding whether there's
  anything releasable at all), and the build only happens
  conditionally, after, only when a release was actually cut - no
  wasted build when there's nothing to release.
- The asset-upload step already read from the job's own freshly-built
  dist/ (never a committed file), so no change needed there beyond the
  reordering.

Also untracked the two dist/script.user.js files this repo had
force-committed historically (graphite-to-github-button, and
github-to-graphite-button as added by this same PR) - both are now
build-only like every other package's dist output, consistent with
the release-only model. Nothing regenerates or re-commits them going
forward.

This removes the need for bypass-actor coverage on a dist-commit step
entirely. release-it's own version-bump commit+tag still needs to push
straight to protected main as the automation bot - that bypass
question is being handled separately at the org level (task #103,
fixing it at the nsheaps/.github settings-sync source rather than
patching the live ruleset directly), not re-litigated in this PR.

Verified: full clean build still succeeds (Nx cache correctly restores
identical output after deleting dist/ locally - checked content, not
just cache-hit claim); `git status` confirms dist/ stays untracked
after a fresh build; `release-it --ci --dry-run` re-run shows the
changeset now contains only the intended file changes (workflow +
the two dist untrackings), with no dist noise going forward once this
commit lands.
…plate

packages/_template and packages/template were near-identical scaffolds, both
introduced by the same initial commit (7ff42bb) and both undocumented. Keep
packages/template: ci.yml already refers to it by that exact name as the
package whose placeholder `exit 1` test script keeps a repo-wide test gate
from being wired up yet.

Refs #25
typescript was pinned identically (~6.0.3) in every package as well as the
root. Yarn workspace hoisting already makes the root copy resolvable from
every package, so the per-package entries were redundant duplication that had
to be bumped in lockstep. Genuinely package-specific devDependencies
(@types/greasemonkey, @types/node) are left alone.

The two greasyforkPublish packages' copies are removed in the following
commit, which rewrites those same package.json files' build scripts.

Refs #25
…"use strict"

Each userscript's metadata block moves out of the top of src/index.ts and into
a per-package src/meta.json. scripts/build-userscript.mjs renders the
`// ==UserScript==` block from it - taking @Version from the package's own
package.json - and prepends it to the tsc-compiled dist/index.js to produce
dist/script.user.js.

This replaces the Node one-liner that string-stripped tsc's leading
`"use strict";` line. That prologue only needed removing because the header
was committed inside the TS source and so ended up one line below the top of
the file; real Greasemonkey silently ignores a metablock that isn't the
literal first thing in the file. Generating the header post-compile makes it
unconditionally first and lets tsc's prologue simply land underneath it.
Switching tsc to module mode was the other option and is not viable: it emits
a bare `export {};` marker, a SyntaxError once a userscript manager injects
the file as a classic script.

package.json is the only source of the version, so the builder rejects a
meta.json that declares one. nx's build inputs gain package.json and the
builder itself, so a version bump or a change to the renderer invalidates the
cached build rather than re-emitting a stale header.

Also drops the now-redundant per-package typescript devDependency from these
two packages (see previous commit) and rewrites the tsconfig comments, whose
"use strict"-stripping rationale no longer applies; the sourceMap: false
rationale still does and is kept.

Refs #25
…release-it

Replaces the single repo-wide release-it config with a shared
.release-it.base.json plus a per-package .release-it.js in each
greasyforkPublish package, so each script owns its own version and its own
CHANGELOG.md instead of sharing one repo-wide version and changelog.

Each package's package.json is now the single source of truth for its version.
The base config uses release-it's native npm plugin with
`npm: { publish: false }` rather than @release-it/bumper - bumper is only
needed when the version lives in a non-package.json manifest, which is why the
marketplace repos this pattern came from need it and we don't. Note the old
root config set `npm: false`, which disables the npm plugin outright including
version bumping; that is why the repo-wide version was never actually bumped
and releases were driven purely by git tags.

The conventional-changelog preset carries over unchanged, with
gitRawCommitsOpts.path scoping each changelog to commits that touched that
package - otherwise every package's CHANGELOG would list the whole monorepo's
history. Committing, tagging and pushing are disabled per package; the release
workflow aggregates them into one commit and one push.

github-actions-grafana-jump is internal-only (no greasyforkPublish field) and
deliberately gets none of this.

Refs #25
Adds a root mise.toml pinning node 24.18.0 (matching .nvmrc and
package.json's engines.node) and yarn 4.17.0 (matching package.json's
packageManager), so a local checkout and CI resolve the same toolchain from
one file. jq is pinned too - scripts/auto-bump-packages.sh uses it to build
its JSON summary, and pinning it here means local runs match the runners'
preinstalled copy.

CI swaps actions/setup-node for jdx/mise-action, which also removes the
separate corepack enable/install step since mise provisions yarn directly.

Refs #25
…on PRs

Rewrites release.yaml around scripts/auto-bump-packages.sh, which patch-bumps
each greasyforkPublish package whose own files changed since a base ref and
preserves any package a human already bumped higher. Change detection excludes
each package's package.json and CHANGELOG.md so a bump commit cannot
re-trigger a bump for itself.

On a pull request the script runs with --preview (arithmetic next-patch, no
release-it invocation, no writes) and posts a sticky Package/Base/New/Action
table. Nothing is committed to the PR branch.

On push to main a single job bumps, lints, commits once, builds, and does one
atomic push of the bump commit plus the release tags. Doing all of it in one
push is what prevents a bump from triggering a follow-up lint commit that
would trigger another bump. Change detection uses a moving release/last-run
tag, which replaces the old before/after `git describe` comparison - that only
worked when a single repo-wide tag was cut per run.

Releases stay one-per-run rather than one-per-package. Every script's
@downloadurl points at the stable releases/latest/download/<name>.user.js
path, so per-package tags would make "latest" ambiguous and leave some
scripts' URLs resolving to a release with no asset for them; one release per
run carries every bumped script's asset. Per-package provenance lives in each
package's own version and CHANGELOG.md. This judgement call is documented
inline in the workflow.

Refs #25
The README still documented Changesets, which this repo has not used since it
moved to release-it, and an nvm/corepack setup that mise now replaces.
Documents how to add a new script (copy packages/template, which was
previously undocumented), where a script's UserScript metadata now lives, and
how per-package versioning and releases work.

Refs #25
…ersion

Also drops package.json's leftover "version": "0.1.0" field - dead weight
now that this branch has moved to per-package versioning with no
repo-wide version.
…le change

The "did anything else change" filter ran before the "did a human already
bump this package's version" check, and the filter ignores package.json.
A package bumped by hand with no other file touched fell through the
filter's continue before ever reaching the already-bumped branch, so its
higher version was silently never released - despite the script's own
docs and the README both promising a manual bump is always respected.

Moved the already-bumped check first so it runs regardless of what else
changed; the file-change filter now only gates the normal patch-bump path.
Every other action in ci.yml/release.yaml is pinned to a commit SHA;
this one was left on a floating branch ref despite being the step that
mints the automation GitHub App token with contents: write, used to
push to protected main and cut releases. nsheaps/github-actions has no
tags, so pinned to that repo's current main tip.

docs(readme): correct the GreasyFork auto-update claim

The README claimed the GitHub Release's stable download URL "is how
GreasyFork picks up new versions," but GreasyFork forcibly rewrites
@downloadURL/@updateURL for any script actually listed on its site (this
was the whole reason the pipeline targets GitHub Releases directly
instead of GreasyFork's own update infra). Only a direct-from-GitHub
install benefits from the new URL; GreasyFork's existing listings keep
updating through GreasyFork's own mechanism, unaffected by this pipeline.
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Userscript Version Preview

Preview only — versions and CHANGELOGs are bumped automatically on merge to main, not in this PR. Manual bumps to a higher version are preserved.

Package Base New Action
github-to-graphite-button 0.3.1 0.3.2 will-bump
graphite-to-github-button 0.3.3 0.3.4 will-bump

claude added 2 commits August 3, 2026 21:32
ci.yml's own comment deferred running `nx run-many --target=test --all`
because packages/template's placeholder test script always failed
(`exit 1`), which would break the gate on that scaffold package alone.
Rather than leave tests unwired, give every package without real
coverage yet a no-op passing `test` script (template, _template, and
the two userscript packages) and add a Test step to ci.yml. Real
coverage (github-actions-grafana-jump's `node --test`) is untouched.

Also restores packages/_template, removed by an earlier commit on this
same branch as a "duplicate" of packages/template. Brought back exactly
as it existed on main (name, UserScript header, tsconfig), minus the
now-redundant per-package `typescript` devDependency that this branch
already hoisted to the workspace root for every other package. Added
its yarn.lock workspace entry back by hand alongside it - no network
access in this environment to regenerate the lockfile via `yarn install`.
Rewrote a few comments that were denser than they needed to be -
saying things plainly instead of using terms like "scaffold", "no-op
passing test", or "coverage" when a simpler sentence works just as well.
@nsheaps-oura
nsheaps-oura marked this pull request as ready for review August 3, 2026 22:14
@nsheaps-oura
nsheaps-oura merged commit 6b116ae into main Aug 3, 2026
5 checks passed
@nsheaps-oura
nsheaps-oura deleted the claude/pr-25-completion-ulo62c branch August 3, 2026 22:14
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.

2 participants