Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions .github/workflows/regen-userscripts.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
name: Regenerate Compiled Userscripts

# Keeps every package's committed dist/*.user.js in sync with its TypeScript
# source. dist/ is gitignored workspace-wide, but each package that publishes
# a userscript force-commits its dist/*.user.js because Greasy Fork's sync
# mechanism points at a stable raw.githubusercontent.com URL for it — see
# graphite-to-github-button's history (4fc2806 renamed the raw .ts source
# straight to this path without ever compiling it; this workflow exists so
# that class of bug can't silently recur, now for every package, not just
# that one).
#
# This repo is an Nx monorepo (yarn workspaces "packages/*", nx.json's
# "build" target cached per-project). Rather than hardcoding one package's
# build command, this always builds every project (`yarn run build`, i.e.
# `nx run-many --target=build --all` — the same command ci.yml already
# runs) and then stages whatever dist/*.user.js artifacts actually changed.
# We intentionally skip `nx affected` here: computing a correct base/head
# for a push-triggered affected run adds real complexity (fetch-depth,
# first-push-to-a-branch edge cases), and with only a handful of packages a
# full build is cheap. Revisit if the package count grows enough that this
# stops being true.
#
# Loop safety (same pattern as nsheaps/github-actions' sync-plugin-specs
# reusable workflow): two independent layers —
# 1. The `paths` filter below matches any package's source/config files
# but excludes packages/**/dist/**, so the bot's own commit (which
# only touches dist/*.user.js files) can't re-trigger this workflow.
# 2. The staging step only commits/pushes when the rebuild actually
# changed a tracked dist/*.user.js. Rebuilding from unchanged source
# is deterministic, so even a manual workflow_dispatch re-run right
# after a bot commit is a no-op.
#
# Only packages that actually produce a dist/*.user.js are ever staged —
# packages/_template and packages/template build fine (same as in ci.yml)
# but produce no *.user.js output, so they're naturally never touched by
# the staging step below; no special-casing needed. Likewise,
# github-actions-grafana-jump builds fine but has no *.user.js output of
# its own yet either.
#
# main is protected by an active `require-pr` ruleset. This repo's
# settings.yml grants the org automation GitHub App an always-exemption
# (bypass_mode: always) on that ruleset — same as repo admins — so
# authenticating as that app via checkout-as-app (the same action
# apply-repo-settings.yaml uses) should let this push straight to main.
# The branch+PR fallback below is kept as defense-in-depth: the live
# ruleset hasn't been confirmed to actually reflect that settings.yml
# exemption yet (see task #45's bypass_actors sync-gap investigation),
# so if the push is still rejected, this falls back to the same
# "try direct push, fall back to PR" idiom as nsheaps/github-actions'
# sync-main-to-edge reusable workflow, opening a PR via that repo's
# open-pr-if-needed composite action (idempotent — reuses an already-open PR
# instead of piling up duplicates).

on:
push:
branches: [main]
paths:
- "packages/**"
- "!packages/**/dist/**"
- "tsconfig.base.json"
- ".github/workflows/regen-userscripts.yaml"
workflow_dispatch:

permissions:
contents: read

concurrency:
# Serialize per-ref so a fast follow-up push waits for the in-flight run
# instead of racing it on the same branch.
group: regen-userscripts-${{ github.ref }}
cancel-in-progress: false

jobs:
regen:
name: Rebuild compiled userscripts
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout as automation bot
uses: nsheaps/github-actions/.github/actions/checkout-as-app@main
with:
app-id: ${{ secrets.AUTOMATION_GITHUB_APP_ID }}
private-key: ${{ secrets.AUTOMATION_GITHUB_APP_PRIVATE_KEY }}

- name: Set up Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: ".nvmrc"

- name: Set up corepack
run: corepack enable && corepack install

- name: Install dependencies
run: yarn install --immutable

- name: Build all packages
run: yarn run build

- name: Stage regenerated userscripts if any changed
id: stage
run: |
mapfile -t USER_SCRIPTS < <(find packages -maxdepth 3 -type f -path "*/dist/*.user.js")
if [ "${#USER_SCRIPTS[@]}" -eq 0 ]; then
echo "No packages/*/dist/*.user.js build artifacts found; nothing to stage."
echo "changed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
git add -f "${USER_SCRIPTS[@]}"
if git diff --cached --quiet; then
echo "Compiled userscripts already up to date; nothing to commit."
echo "changed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
git commit -m "chore: regenerate compiled userscripts"
echo "changed=true" >> "$GITHUB_OUTPUT"

- name: Try direct push to main
if: steps.stage.outputs.changed == 'true'
id: push
continue-on-error: true
run: git push origin HEAD:main

- name: Push to a branch if direct push was blocked
if: steps.stage.outputs.changed == 'true' && steps.push.outcome == 'failure'
id: branch
env:
BRANCH: bot/regen-userscripts
run: |
git push -f origin "HEAD:refs/heads/${BRANCH}"
echo "branch=${BRANCH}" >> "$GITHUB_OUTPUT"

- name: Open PR if direct push was blocked
if: steps.stage.outputs.changed == 'true' && steps.push.outcome == 'failure'
uses: nsheaps/github-actions/.github/actions/open-pr-if-needed@main
with:
title: "chore: regenerate compiled userscripts"
body: |
Direct push to `main` was blocked, so the regenerated
compiled userscript(s) are going through a PR instead.
base: main
head: ${{ steps.branch.outputs.branch }}
token: ${{ secrets.GITHUB_TOKEN }}
2 changes: 1 addition & 1 deletion packages/github-actions-grafana-jump/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
},
"devDependencies": {
"@types/greasemonkey": "~4.0.7",
"typescript": "~5.8.3"
"typescript": "~6.0.3"
}
}
8 changes: 7 additions & 1 deletion packages/github-actions-grafana-jump/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
"rootDir": "./src",
// This is the only package that references ambient globals from
// @types/node (the `module` test-export guard) and @types/greasemonkey
// (`GM`). Under moduleResolution: bundler, tsc does not auto-include
// these from node_modules/@types the way it does under "node"/"node10" -
// list them explicitly rather than relying on that auto-discovery.
"types": ["node", "greasemonkey"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
Expand Down
78 changes: 32 additions & 46 deletions packages/graphite-to-github-button/dist/script.user.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,57 +11,43 @@
// @downloadURL https://update.greasyfork.org/scripts/509841/Graphite%20%3D%3E%20GitHub%20button.user.js
// @updateURL https://update.greasyfork.org/scripts/509841/Graphite%20%3D%3E%20GitHub%20button.meta.js
// ==/UserScript==

const PATH_REGEX = /^\/github\/pr\/([^\/]+)\/([^\/]+)\/([^\/]+).*$/;
const SELECTOR =
'[class^="PullRequestTitleBar_container_"] > div:nth-child(1) > div:nth-child(2)';

const addButton = (toolbar: HTMLElement) => {
const match = window.location.pathname.match(PATH_REGEX);
if (!match) return;

const [_, org, repo, pr] = match;
const gitHubLink = `https://github.com/${org}/${repo}/pull/${pr}`;

if (document.getElementById("gitHubLink") != null) {
return;
}

const anchorEl = document.createElement("a");
anchorEl.setAttribute("id", "gitHubLink");
anchorEl.setAttribute("href", gitHubLink);
anchorEl.setAttribute("target", "_blank");
anchorEl.setAttribute(
"style",
"background: #f0f0f333; padding: 6px; border-radius: 4px; flex-shrink: 0;"
);
anchorEl.appendChild(document.createTextNode("GitHub ↗️"));

toolbar.appendChild(anchorEl);
const SELECTOR = '[class^="PullRequestTitleBar_container_"] > div:nth-child(1) > div:nth-child(2)';
const addButton = (toolbar) => {
const match = window.location.pathname.match(PATH_REGEX);
if (!match)
return;
const [_, org, repo, pr] = match;
const gitHubLink = `https://github.com/${org}/${repo}/pull/${pr}`;
if (document.getElementById("gitHubLink") != null) {
return;
}
const anchorEl = document.createElement("a");
anchorEl.setAttribute("id", "gitHubLink");
anchorEl.setAttribute("href", gitHubLink);
anchorEl.setAttribute("target", "_blank");
anchorEl.setAttribute("style", "background: #f0f0f333; padding: 6px; border-radius: 4px; flex-shrink: 0;");
anchorEl.appendChild(document.createTextNode("GitHub ↗️"));
toolbar.appendChild(anchorEl);
};

const toolbarObserver = new MutationObserver((_, observer) => {
const toolbar = document.querySelector(SELECTOR) as HTMLElement;
if (toolbar) {
observer.disconnect();
addButton(toolbar);
}
const toolbar = document.querySelector(SELECTOR);
if (toolbar) {
observer.disconnect();
addButton(toolbar);
}
});

let lastPathname: string | undefined;
let lastPathname;
const routeChangeObserver = new MutationObserver(() => {
const { pathname } = window.location;

if (pathname !== lastPathname) {
lastPathname = pathname;

if (pathname.match(PATH_REGEX)) {
toolbarObserver.observe(document.body, {
childList: true,
subtree: true,
});
const { pathname } = window.location;
if (pathname !== lastPathname) {
lastPathname = pathname;
if (pathname.match(PATH_REGEX)) {
toolbarObserver.observe(document.body, {
childList: true,
subtree: true,
});
}
}
}
});

routeChangeObserver.observe(document.body, { childList: true, subtree: true });
2 changes: 1 addition & 1 deletion packages/graphite-to-github-button/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "0.3.3",
"main": "dist/index.js",
"scripts": {
"build": "tsc --build",
"build": "tsc --build && node -e \"const fs=require('fs');const c=fs.readFileSync('dist/index.js','utf8').replace(/^[\\\"']use strict[\\\"'];?\\r?\\n/,'');fs.writeFileSync('dist/script.user.js',c)\"",
"lint": "oxlint"
},
"devDependencies": {
Expand Down
67 changes: 67 additions & 0 deletions packages/graphite-to-github-button/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// ==UserScript==
// @name Graphite => GitHub button
// @description Add a button to go from app.graphite.dev to github.com
// @match https://app.graphite.dev/*
// @version 0.3.3
// @run-at document-start
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant none
// @license MIT
// @namespace https://app.graphite.dev
// @downloadURL https://update.greasyfork.org/scripts/509841/Graphite%20%3D%3E%20GitHub%20button.user.js
// @updateURL https://update.greasyfork.org/scripts/509841/Graphite%20%3D%3E%20GitHub%20button.meta.js
// ==/UserScript==

const PATH_REGEX = /^\/github\/pr\/([^\/]+)\/([^\/]+)\/([^\/]+).*$/;
const SELECTOR =
'[class^="PullRequestTitleBar_container_"] > div:nth-child(1) > div:nth-child(2)';

const addButton = (toolbar: HTMLElement) => {
const match = window.location.pathname.match(PATH_REGEX);
if (!match) return;

const [_, org, repo, pr] = match;
const gitHubLink = `https://github.com/${org}/${repo}/pull/${pr}`;

if (document.getElementById("gitHubLink") != null) {
return;
}

const anchorEl = document.createElement("a");
anchorEl.setAttribute("id", "gitHubLink");
anchorEl.setAttribute("href", gitHubLink);
anchorEl.setAttribute("target", "_blank");
anchorEl.setAttribute(
"style",
"background: #f0f0f333; padding: 6px; border-radius: 4px; flex-shrink: 0;"
);
anchorEl.appendChild(document.createTextNode("GitHub ↗️"));

toolbar.appendChild(anchorEl);
};

const toolbarObserver = new MutationObserver((_, observer) => {
const toolbar = document.querySelector(SELECTOR) as HTMLElement;
if (toolbar) {
observer.disconnect();
addButton(toolbar);
}
});

let lastPathname: string | undefined;
const routeChangeObserver = new MutationObserver(() => {
const { pathname } = window.location;

if (pathname !== lastPathname) {
lastPathname = pathname;

if (pathname.match(PATH_REGEX)) {
toolbarObserver.observe(document.body, {
childList: true,
subtree: true,
});
}
}
});

routeChangeObserver.observe(document.body, { childList: true, subtree: true });
23 changes: 22 additions & 1 deletion packages/graphite-to-github-button/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,28 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
"rootDir": "./src",
// This package's src/index.ts has no import/export statements, so tsc
// treats it as a global script rather than a module and (because
// "strict" implies "alwaysStrict") prepends a `"use strict";` line to
// the compiled output. That line would sit above the
// `// ==UserScript==` header block that Greasy Fork/userscript managers
// scan for, which is meant to be the first thing in the file.
//
// We used to disable this via `"alwaysStrict": false`, but that value
// is being deprecated/removed upstream, so instead we let tsc emit the
// line as normal and strip it in this package's own `build` script
// (see package.json) when copying dist/index.js to dist/script.user.js
// — that's resilient regardless of whether a future TypeScript keeps
// an escape hatch for this at all.
//
// dist/script.user.js is the only build artifact committed and served
// (via a stable raw.githubusercontent.com URL) to Greasy Fork; the
// sibling index.js.map is not shipped alongside it. Skip emitting a
// sourceMap for this package so the compiled output doesn't end with a
// `//# sourceMappingURL=...` comment pointing at a file that won't be
// there for end users.
"sourceMap": false
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM"],
"moduleResolution": "node",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ __metadata:
resolution: "@nsheaps/gm-github-actions-grafana-jump@workspace:packages/github-actions-grafana-jump"
dependencies:
"@types/greasemonkey": "npm:~4.0.7"
typescript: "npm:~5.8.3"
typescript: "npm:~6.0.3"
languageName: unknown
linkType: soft

Expand Down