Skip to content

Commit 71130d4

Browse files
ralyodioclaude
andcommitted
The repo is public: restore CI, and install without a key
Two things follow from opening this up, and both were written into the repo as "private" facts that are now false. Actions is back. It was deleted on 2026-08-29 because the job was refused before a runner was ever allocated: an unpaid balance on the profullstack org suspends Actions compute on private repos. As the previous commit established, the balance is not this repo's CI minutes -- those are free -- it is artifact storage and Code Quality credits run up elsewhere. Nothing about that balance has been paid. It is still owed and still suspends every private repo in the org. The suspension simply does not reach public repos, so the one thing standing between this workflow and a runner is gone. Removing it was right while it could not run; keeping it removed now is not. The workflow runs bin/scripts-check rather than restating the checks in YAML, which is what the old one did -- three loops in a script and the same three loops in a workflow, free to drift. The pre-push hook stays: it is the copy that runs before the push, and --no-verify now only defers the same command to the runner instead of skipping it. install.sh clones over HTTPS by default, so a new box needs no key and no account: curl -fsSL https://raw.githubusercontent.com/profullstack/scripts/main/install.sh | sh That also ends the chicken-and-egg this repo carried, where provision-ssh-keys could only be fetched with the key it exists to install. --ssh (or SCRIPTS_REPO) still gets a pushable checkout; an existing checkout keeps its own origin, so the dev box is untouched. Verified by cloning with the credential helper disabled and GIT_TERMINAL_PROMPT=0, and by a --dry-run resolving v0.2.1 anonymously off the remote. README and all three file headers updated: they said there is no CI and no anonymous install, in detail and on purpose, which would have been the most misleading thing in the repo to a first reader. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EP9qibz6eUB3wB8sGHCVS5
1 parent 8720eab commit 71130d4

5 files changed

Lines changed: 164 additions & 60 deletions

File tree

‎.githooks/pre-push‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
#!/usr/bin/env bash
22
# Run the release gate before anything leaves this machine.
33
#
4-
# This is where the gate lives now. It was a GitHub Actions workflow until
5-
# 2026-08-29, when the job was refused before it ever started: an unpaid balance
6-
# on the profullstack org suspends Actions compute on private repos.
4+
# CI runs this same `bin/scripts-check` on every push and tag, so the hook is
5+
# not the only gate any more. It stays because it is the one that runs before
6+
# the push rather than after it: a syntax error caught here never reaches the
7+
# remote at all, and every tool in bin/ is a symlink into a working tree, so a
8+
# broken one is on PATH on every box at the next upgrade.
9+
#
10+
# For a while it was the only gate. The workflow was deleted on 2026-08-29,
11+
# when the job was refused before it ever started: an unpaid balance on the
12+
# profullstack org suspends Actions compute on private repos.
713
#
814
# Note the balance is NOT this repo's CI minutes -- those are covered by the
915
# included allowance at net $0.00. It is Actions artifact storage and Code
1016
# Quality credits run up on unrelated repos. So deleting the workflow saved no
1117
# money; it was removed because a gate that cannot run is worse than no gate.
12-
#
13-
# Nothing here needs a hosted runner anyway -- no build, no matrix, no secret --
14-
# so a runner to execute `bash -n` was the wrong shape regardless.
18+
# The balance is still owed, and still suspends every private repo in the org.
19+
# This one is public, which is why CI could come back here and not there.
1520
#
1621
# Set up by install.sh (`git config core.hooksPath .githooks`). To run the same
1722
# checks by hand: `scripts-check`. To push past it in an emergency:

‎.github/workflows/release.yml‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# The release gate, and the release itself.
2+
#
3+
# This workflow existed before, was deleted on 2026-08-29, and is back. It was
4+
# never broken: profullstack is on the free plan, this repo was private, and
5+
# GitHub refused the job for billing before a runner was ever allocated. That
6+
# surfaces as a red X with no logs, which reads exactly like a broken workflow.
7+
# The repo is public now and Actions minutes are free on public repos, so the
8+
# job can actually run — which is the only thing that ever stopped it.
9+
#
10+
# The gate is `bin/scripts-check`, not a copy of it. The previous version of
11+
# this file inlined the same three loops in YAML, so CI and the local hook were
12+
# two implementations of one rule and free to drift. Here the runner runs the
13+
# same file `.githooks/pre-push` does; the hook stays because feedback before a
14+
# push beats feedback after one, not because CI is untrusted.
15+
#
16+
# What a release means here: install.sh and scripts-upgrade resolve the newest
17+
# `v*` tag on the remote and check the tree out at it. The TAG is the artifact.
18+
# There is deliberately no tarball, because nothing would ever download one.
19+
name: release
20+
21+
on:
22+
push:
23+
branches: [main]
24+
tags: ['v*']
25+
pull_request:
26+
27+
permissions:
28+
contents: write
29+
30+
jobs:
31+
check:
32+
runs-on: ubuntu-latest
33+
steps:
34+
- uses: actions/checkout@v4
35+
36+
# scripts-check skips shellcheck with a note when it is absent, so
37+
# without this step the lint third of the gate would quietly not run.
38+
- name: Install shellcheck
39+
run: sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck
40+
41+
- name: scripts-check
42+
run: bin/scripts-check
43+
44+
release:
45+
needs: check
46+
if: startsWith(github.ref, 'refs/tags/v')
47+
runs-on: ubuntu-latest
48+
steps:
49+
- uses: actions/checkout@v4
50+
with: { fetch-depth: 0 }
51+
52+
- name: Create the release
53+
env:
54+
GH_TOKEN: ${{ github.token }}
55+
run: |
56+
set -euo pipefail
57+
tag="${GITHUB_REF_NAME}"
58+
if gh release view "$tag" >/dev/null 2>&1; then
59+
echo "release $tag already exists"; exit 0
60+
fi
61+
gh release create "$tag" \
62+
--title "$tag" \
63+
--generate-notes

‎README.md‎

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,21 @@ Hand-written command-line tools. Everything in `bin/` is meant to sit on `PATH`.
55
## Install
66

77
```sh
8-
git clone git@github.com:profullstack/scripts.git ~/scripts
9-
sh ~/scripts/install.sh
8+
curl -fsSL https://raw.githubusercontent.com/profullstack/scripts/main/install.sh | sh
109
```
1110

12-
The repo is private, so there is no anonymous `curl | sh`: the clone needs a key
13-
GitHub already knows. On a brand-new box that has to come first, which is the one
14-
chicken-and-egg here, since `provision-ssh-keys` is itself in this repo.
11+
That is the whole thing on a brand-new box: the repo is public, so the clone
12+
needs no key and no account. It also ends the chicken-and-egg this repo used to
13+
have, where `provision-ssh-keys` — the tool that puts our keys on a machine —
14+
could only be fetched using the key it exists to install.
15+
16+
Cloning by hand works the same way, and `--ssh` gets you a checkout you can push
17+
from:
18+
19+
```sh
20+
git clone https://github.com/profullstack/scripts.git ~/scripts
21+
sh ~/scripts/install.sh # or: sh ~/scripts/install.sh --ssh
22+
```
1523

1624
`install.sh` clones (or updates) the checkout, moves it to the newest release
1725
tag, and symlinks every executable in `bin/` into `~/.local/bin`. Symlinking
@@ -21,6 +29,7 @@ rather than copying keeps `~/.local/bin` and this repo from drifting apart.
2129
sh install.sh # newest release
2230
sh install.sh --edge # track main instead
2331
sh install.sh --ref v0.2.0 # a specific tag
32+
sh install.sh --ssh # clone over SSH, for a checkout you push from
2433
sh install.sh --dry-run # say what would happen, change nothing
2534
```
2635

@@ -43,31 +52,35 @@ and checks the tree out at it. Cutting a release is therefore exactly:
4352
git tag -a v0.2.0 -m "..." && git push origin v0.2.0
4453
```
4554

46-
The gate runs before the push, not after it. `.githooks/pre-push` runs
47-
`scripts-check`, which parses every script by shebang, verifies the executable
48-
bits, and runs `shellcheck -S error` over the shell ones. Then publish the
49-
release object:
50-
51-
```sh
52-
gh release create v0.2.0 --title "v0.2.0 — ..." --notes-file notes.md
53-
```
54-
55-
**There is no CI, on purpose.** This was a GitHub Actions workflow until
56-
2026-08-29, when the job was refused before it ever ran: an unpaid balance on
57-
the `profullstack` org suspends Actions compute on private repos. It failed as a
58-
red X with no logs, which reads exactly like a broken workflow and is not one,
59-
and a gate that does not run is worse than no gate, because the X implies
60-
something was checked.
61-
62-
Worth being precise about the money, because it is easy to assume wrongly: **this
55+
Pushing the tag is enough. `.github/workflows/release.yml` runs the gate and
56+
then creates the release object, so `gh release create` by hand is only a
57+
fallback for when Actions is unavailable.
58+
59+
**The gate runs twice, and that is deliberate.** `bin/scripts-check` is the
60+
rule — it parses every script by shebang, verifies the executable bits, and runs
61+
`shellcheck -S error` over the shell ones. `.githooks/pre-push` runs it before
62+
anything leaves the machine, and CI runs *that same file* on the runner. Neither
63+
is a re-implementation of the other, which is how the two used to be free to
64+
drift; the hook stays because feedback before a push beats feedback after one.
65+
66+
**A note on the red X in the history, and why CI came back.** This workflow was
67+
deleted on 2026-08-29 and restored when the repo was opened up. It was never
68+
broken YAML: an unpaid balance on the `profullstack` org suspends Actions
69+
compute, and the job was refused before a runner was ever allocated — a red X
70+
with no logs, which is indistinguishable from a workflow that ran and failed. A
71+
gate that cannot run is worse than no gate, because the X implies something was
72+
checked, so it was removed rather than left there lying.
73+
74+
Be precise about the money, because the obvious assumption is wrong: **this
6375
repo's CI minutes were never the cost.** They are covered by the included
6476
allowance at net $0.00. The balance is Actions artifact storage and Code Quality
65-
credits accrued on unrelated repositories. Deleting this workflow therefore saved
66-
nothing on the bill, and clearing that balance would let Actions run again
67-
everywhere.
77+
credits accrued on unrelated repositories, so deleting this workflow saved
78+
nothing on the bill and was never going to.
6879

69-
It stays deleted regardless, because nothing here needs a hosted runner: no
70-
build, no matrix, no secret, just three loops over files already on disk.
80+
What changed is not the balance — it is still owed, and every *private* repo in
81+
the org is still suspended by it. The suspension only applies to private repos.
82+
This one is public now, so the single thing standing between this workflow and a
83+
runner is gone.
7184

7285
**A development checkout is left alone.** Because `~/.local/bin/*` are symlinks
7386
into the working tree, the command on PATH is whatever the checkout is at, so
@@ -243,7 +256,8 @@ scripts-check --quiet # only complain
243256
```
244257

245258
`.githooks/pre-push` runs it before every push, wired up by `install.sh` via
246-
`git config core.hooksPath .githooks`. `git push --no-verify` bypasses it.
259+
`git config core.hooksPath .githooks`. `git push --no-verify` bypasses it — and
260+
CI then runs the same command on the runner, which does not.
247261

248262
A missing `shellcheck` is skipped with a note rather than failing — an absent
249263
linter must not be able to block a release.

‎bin/scripts-check‎

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,28 @@
44
# scripts-check # check the checkout this command lives in
55
# scripts-check --quiet # only complain
66
#
7-
# This used to be a GitHub Actions workflow. It is not any more: an unpaid
8-
# balance on the profullstack org suspends Actions compute on private repos, so
9-
# the job was refused before it ever ran -- a red X with no logs, which reads
10-
# exactly like a broken workflow and is not one. A gate that does not run is
11-
# worse than no gate, because the red X implies something was checked.
7+
# This file is the rule, and everything else calls it. `.githooks/pre-push`
8+
# runs it before anything leaves the machine, and .github/workflows/release.yml
9+
# runs this same file on the runner rather than re-stating the checks in YAML —
10+
# which is what the two used to do, leaving CI and the hook free to drift.
11+
#
12+
# For a while there was no CI half at all. The workflow was deleted on
13+
# 2026-08-29, while the repo was private: an unpaid balance on the profullstack
14+
# org suspends Actions compute on private repos, so the job was refused before
15+
# it ever ran -- a red X with no logs, which reads exactly like a broken
16+
# workflow and is not one. A gate that does not run is worse than no gate,
17+
# because the red X implies something was checked.
1218
#
1319
# To be accurate about the money: this repo's CI minutes were never the cost.
1420
# They are covered by the included allowance at net $0.00. The balance is
15-
# Actions artifact storage and Code Quality credits on unrelated repos. Moving
16-
# the gate here saved nothing on the bill and was not meant to.
21+
# Actions artifact storage and Code Quality credits on unrelated repos, so
22+
# moving the gate here saved nothing on the bill and was not meant to. The
23+
# balance is still owed; the suspension just does not reach public repos, which
24+
# is why opening this one up brought CI back.
1725
#
18-
# So it runs here instead, and `.githooks/pre-push` runs it before every push.
19-
# Nothing about it needed a hosted runner: there is no build, no matrix, and
20-
# no secret. It is three loops over files that are already on disk.
26+
# Nothing here ever needed a hosted runner: no build, no matrix, no secret. It
27+
# is three loops over files that are already on disk, which is exactly why the
28+
# same three loops can run in both places at no cost.
2129
#
2230
# What it checks, and why each one:
2331
#

‎install.sh‎

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,33 @@
44
# sh install.sh # latest release tag
55
# sh install.sh --edge # track main instead
66
# sh install.sh --ref v0.2.0 # a specific tag or branch
7+
# sh install.sh --ssh # clone over SSH, for a checkout you push from
78
# sh install.sh --dry-run # say what would happen, change nothing
89
#
9-
# The repo is private, so there is no anonymous curl one-liner: this clones
10-
# over SSH and relies on the key that is already on the box. On a fresh
11-
# machine that means provisioning a key first — bin/provision-ssh-keys is the
12-
# tool for that, which is a chicken-and-egg only the very first time.
10+
# The repo is public, so a fresh box needs no key and no account:
1311
#
14-
# ssh -T git@github.com # must not say "Permission denied"
15-
# git clone git@github.com:profullstack/scripts.git ~/scripts
16-
# sh ~/scripts/install.sh
12+
# curl -fsSL https://raw.githubusercontent.com/profullstack/scripts/main/install.sh | sh
13+
#
14+
# That is the whole install. Cloning over HTTPS also ends the chicken-and-egg
15+
# this file used to carry: provision-ssh-keys lives in this repo, so getting it
16+
# onto a new machine used to need the very key it exists to install.
17+
#
18+
# Use --ssh (or SCRIPTS_REPO) for a checkout you intend to push from. An HTTPS
19+
# clone reads fine and upgrades fine; it is just not set up to write.
1720
#
1821
# POSIX sh on purpose. This is the one file that runs before anything is
1922
# installed, possibly on a box whose only shell is dash, so it may not assume
2023
# bash the way the tools in bin/ do.
2124
#
22-
# There is no CI. The gate that used to be a GitHub Actions workflow is now
23-
# bin/scripts-check, run by .githooks/pre-push, because an unpaid balance on the
24-
# org suspends Actions compute on private repos and the job was refused before
25-
# it started. Not this repo's minutes, which are free -- storage and Code
26-
# Quality credits elsewhere. This script wires the hook up.
25+
# THE GATE RUNS TWICE, ON PURPOSE. bin/scripts-check is the rule. This script
26+
# points core.hooksPath at .githooks so it runs before every push, and
27+
# .github/workflows/release.yml runs that same file on the runner.
28+
#
29+
# CI is back because the repo is public. An unpaid balance on the org suspends
30+
# Actions compute on PRIVATE repos -- not this repo's minutes, which are free;
31+
# storage and Code Quality credits elsewhere -- and while this repo was private
32+
# the job was refused before it ever started. The balance is still owed; public
33+
# repos are simply not subject to it.
2734
#
2835
# WHY A TAG BY DEFAULT. Because `~/.local/bin/*` are symlinks into the working
2936
# tree, the command on PATH is whatever the checkout happens to be at. Tracking
@@ -34,7 +41,12 @@
3441

3542
set -eu
3643

44+
# HTTPS by default: the repo is public, so this works with no key, no account
45+
# and no prior setup, which is the whole point of the curl one-liner above.
46+
# SSH stays a flag away for the boxes that push.
47+
REPO_HTTPS="https://github.com/profullstack/scripts.git"
3748
REPO_SSH="git@github.com:profullstack/scripts.git"
49+
REPO="${SCRIPTS_REPO:-$REPO_HTTPS}"
3850
DIR="${SCRIPTS_DIR:-$HOME/scripts}"
3951
BINDIR="${SCRIPTS_BIN:-$HOME/.local/bin}"
4052
REF="${SCRIPTS_REF:-}"
@@ -43,12 +55,14 @@ DRY=0
4355
while [ $# -gt 0 ]; do
4456
case "$1" in
4557
--edge) REF=main ;;
58+
--ssh) REPO="$REPO_SSH" ;;
59+
--https) REPO="$REPO_HTTPS" ;;
4660
--ref) REF="${2:?--ref needs a tag or branch}"; shift ;;
4761
--ref=*) REF="${1#--ref=}" ;;
4862
--dir) DIR="${2:?--dir needs a path}"; shift ;;
4963
--bin) BINDIR="${2:?--bin needs a path}"; shift ;;
5064
--dry-run|-n) DRY=1 ;;
51-
-h|--help) sed -n '2,27p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
65+
-h|--help) sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
5266
*) echo "install.sh: unknown option $1" >&2; exit 2 ;;
5367
esac
5468
shift
@@ -61,7 +75,7 @@ die() { echo "install.sh: $*" >&2; exit 1; }
6175
# The newest v-tag on the remote, by version sort. Read from the remote rather
6276
# than a local tag list so a stale checkout cannot report an old "latest".
6377
latest_tag() {
64-
git ls-remote --tags --refs "$REPO_SSH" 'v*' 2>/dev/null \
78+
git ls-remote --tags --refs "$REPO" 'v*' 2>/dev/null \
6579
| awk '{print $2}' | sed 's#refs/tags/##' \
6680
| sort -V | tail -1
6781
}
@@ -71,8 +85,8 @@ latest_tag() {
7185
if [ ! -d "$DIR/.git" ]; then
7286
[ -e "$DIR" ] && die "$DIR exists but is not a git checkout; move it aside"
7387
if [ -z "$REF" ]; then REF="$(latest_tag)"; [ -n "$REF" ] || REF=main; fi
74-
say "cloning $REPO_SSH -> $DIR at $REF"
75-
run git clone --quiet "$REPO_SSH" "$DIR"
88+
say "cloning $REPO -> $DIR at $REF"
89+
run git clone --quiet "$REPO" "$DIR"
7690
[ "$DRY" = 1 ] || git -C "$DIR" -c advice.detachedHead=false checkout --quiet "$REF"
7791
else
7892
say "checkout exists at $DIR"

0 commit comments

Comments
 (0)