Skip to content
Merged
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
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: CI

on:
pull_request:
push:
branches: [main]
schedule:
- cron: "17 2 * * 1"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
verify:
name: Verify (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun run typecheck
- run: bun test
- run: bun run build
- if: matrix.os == 'ubuntu-latest'
run: npm pack --dry-run
82 changes: 77 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,17 @@ permissions:
contents: write

jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun run typecheck
- run: bun test

version:
needs: verify
runs-on: ubuntu-latest
outputs:
version: ${{ steps.bump.outputs.version }}
Expand Down Expand Up @@ -62,10 +72,12 @@ jobs:
- name: Build binary
run: |
bun build --compile --target=${{ matrix.target }} --outfile=gh2 src/index.ts
if [[ "${{ matrix.target }}" == bun-darwin-* ]]; then
codesign --remove-signature gh2
codesign --force --deep -s - gh2
fi
case "${{ matrix.target }}" in
bun-darwin-*)
codesign --remove-signature gh2
codesign --force --deep -s - gh2
;;
esac
tar czf ${{ matrix.name }}.tar.gz gh2

- name: Package Debian artifact
Expand Down Expand Up @@ -147,7 +159,7 @@ jobs:
SHA_LINUX_ARM64=$(shasum -a 256 artifacts/gh2-linux-arm64/*.tar.gz | cut -d' ' -f1)
SHA_LINUX_X64=$(shasum -a 256 artifacts/gh2-linux-x64/*.tar.gz | cut -d' ' -f1)

git clone https://x-access-token:${GH_TOKEN}@github.com/circlesac/homebrew-tap.git
git clone "https://x-access-token:${GH_TOKEN}@github.com/circlesac/homebrew-tap.git"
cd homebrew-tap
cp ../homebrew/gh2.rb.template Formula/gh2.rb
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" Formula/gh2.rb
Expand Down Expand Up @@ -175,3 +187,63 @@ jobs:
printf 'push attempt %s failed\n' "$attempt" >&2
done
exit 1

verify-release:
needs: [version, publish-npm, publish-package-indexes]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
env:
VERSION: ${{ needs.version.outputs.version }}
steps:
- uses: actions/setup-node@v4
with:
node-version: 24
registry-url: https://registry.npmjs.org

- name: Verify npm installation
shell: bash
run: |
for attempt in {1..10}; do
if npm install --prefix "$RUNNER_TEMP/npm" "@circlesac/gh2@$VERSION"; then
break
fi
test "$attempt" -lt 10
sleep 15
done
test "$("$RUNNER_TEMP/npm/node_modules/.bin/gh2" --version)" = "$VERSION"
"$RUNNER_TEMP/npm/node_modules/.bin/gh2" doctor --help >/dev/null

- name: Verify standalone installation
shell: bash
run: |
mkdir -p "$RUNNER_TEMP/standalone"
curl -fsSL https://github.com/circlesac/gh2-cli/releases/latest/download/install.sh | INSTALL_DIR="$RUNNER_TEMP/standalone" sh
test "$("$RUNNER_TEMP/standalone/gh2" --version)" = "$VERSION"
"$RUNNER_TEMP/standalone/gh2" doctor --help >/dev/null

- name: Verify Debian package
if: matrix.os == 'ubuntu-latest'
shell: bash
run: |
curl -fsSL -o "$RUNNER_TEMP/gh2.deb" "https://github.com/circlesac/gh2-cli/releases/download/v$VERSION/gh2_${VERSION}_amd64.deb"
dpkg-deb --extract "$RUNNER_TEMP/gh2.deb" "$RUNNER_TEMP/deb"
test "$("$RUNNER_TEMP/deb/usr/bin/gh2" --version)" = "$VERSION"
"$RUNNER_TEMP/deb/usr/bin/gh2" doctor --help >/dev/null

- name: Verify Homebrew installation
if: matrix.os == 'macos-latest'
shell: bash
run: |
for attempt in {1..10}; do
brew update
if brew install circlesac/tap/gh2; then
break
fi
test "$attempt" -lt 10
sleep 15
done
test "$(gh2 --version)" = "$VERSION"
gh2 doctor --help >/dev/null
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ gh2 repo restore <owner/repository> [--yes]
gh2 org pat-policy show <org>
gh2 org pat-policy update <org> [--access restricted|unrestricted] \
[--requests auto|manual] [--max-lifetime none|<days>] [--yes]
gh2 doctor --org <org> [--app <slug>] [--installation <id>] [--support]
gh2 pat login --account <login>
gh2 pat create --account <login> --name <name> --owner <login> \
--repos <all|none|repo,...> --permissions <permission=read|write|admin,...> \
Expand All @@ -59,6 +60,16 @@ settings page before reporting success.
The full personal, organization, repository, and enterprise settings census is
in [`docs/github-admin-gap-census.md`](docs/github-admin-gap-census.md).

### Continuous live verification

`gh2 doctor` runs selected browser-cookie integrations in read-only or dry-run mode and classifies authentication gates separately from parser drift. Use it from a trusted local scheduler because GitHub's sensitive settings pages periodically require sudo authentication and browser cookies must not be stored in GitHub Actions secrets.

```bash
gh2 doctor --org example-org --app example-app --support --output json
```

See [`docs/live-canary.md`](docs/live-canary.md) for the full probe set, exit-code contract, and macOS scheduling guidance.

### GitHub App permissions

Permission changes are a live-authenticated dry run unless `--yes` is present.
Expand Down
2 changes: 1 addition & 1 deletion bin/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function download(url) {
});
}

if (process.env.CI) process.exit(0);
if (process.env.CI || fs.existsSync(path.join(__dirname, "..", ".git"))) process.exit(0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The installer exits whenever the package is installed from a Git checkout or while CI is set, even when bin/native/gh2 is missing. The launcher then imports this installer and immediately attempts to spawn that nonexistent binary, so gh2 fails to start for source-checkout installations and normal CI-installed packages. Only skip release downloads when a source-mode launcher is actually available, or make bin/gh2 fall back to the source entry point. [logic error]

Severity Level: Major ⚠️
- ⚠️ Normal non-CI registry installs remain unaffected.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** bin/install.js
**Line:** 35:35
**Comment:**
	*Logic Error: The installer exits whenever the package is installed from a Git checkout or while `CI` is set, even when `bin/native/gh2` is missing. The launcher then imports this installer and immediately attempts to spawn that nonexistent binary, so `gh2` fails to start for source-checkout installations and normal CI-installed packages. Only skip release downloads when a source-mode launcher is actually available, or make `bin/gh2` fall back to the source entry point.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎


const nativeDir = path.join(__dirname, "native");
const binPath = path.join(nativeDir, "gh2");
Expand Down
23 changes: 23 additions & 0 deletions docs/live-canary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Live contract canary

`gh2 doctor` runs the browser-cookie integrations without submitting any change. It executes only read operations and commands whose default behavior is a dry run, captures their output instead of printing settings data, and reports one status per GitHub surface.

```bash
gh2 app login

gh2 doctor \
--org example-org \
--app example-app \
--installation 12345 \
--support \
--pat-account example-user \
--pat-owner example-org \
--pat-repo example-repo \
--output json
```

Exit `0` means every requested parser reached and understood the live page. Exit `2` means GitHub requires a refreshed browser session or sudo authentication; this is not reported as parser drift. Exit `1` means a form contract changed or another probe failed.

GitHub Actions cannot safely keep a browser session with periodic sudo authentication, so schedule this command on a trusted local machine rather than storing GitHub cookies in repository secrets. Run `gh2 app login` again when the canary reports `reauth_required`, then rerun the same doctor command to distinguish an authentication gate from markup drift.

On macOS, use `launchd` with the absolute Homebrew `gh2` path and write stdout and stderr to a local state directory. Keep the job read-only and point it at a dedicated test App when App settings are included.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"build": "bun build --compile --outfile=dist/gh2 src/index.ts",
"dev": "bun run src/index.ts",
"postinstall": "node bin/install.js",
"test": "vitest run"
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"citty": "^0.2.0"
Expand All @@ -30,5 +31,5 @@
"type": "git",
"url": "https://github.com/circlesac/gh2-cli"
},
"version": "0.0.1"
"version": "0.0.0"
}
18 changes: 18 additions & 0 deletions skills/guide/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,24 @@ permissions and webhook-event subscriptions not named in `--set`. Existing
installations can require a separate owner approval after the App registration is
updated.

## Live contract verification

Run the read-only canary after `gh2 app login` to distinguish GitHub markup drift from an expired or sudo-gated browser session:

```bash
gh2 doctor \
--org example-org \
--app example-app \
--installation 12345 \
--support \
--pat-account example-user \
--pat-owner example-org \
--pat-repo example-repo \
--output json
```

Exit `0` means every requested live parser returned a valid contract, exit `2` means browser or sudo reauthentication is required, and exit `1` means a contract changed or another probe failed. The doctor never passes `--yes`; schedule it only on a trusted local machine because browser cookies must not be stored in GitHub Actions secrets.

### App private keys

```bash
Expand Down
Loading
Loading