@gallopsystems/agent-skills 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugins/copier-template/.claude-plugin/plugin.json +8 -0
- package/plugins/copier-template/skills/copier-template/SKILL.md +68 -0
- package/plugins/copier-template/skills/copier-template/applying-updates.md +87 -0
- package/plugins/copier-template/skills/copier-template/template-authoring.md +89 -0
- package/plugins/doctl/.claude-plugin/plugin.json +2 -2
- package/plugins/doctl/skills/doctl/SKILL.md +74 -48
- package/plugins/doctl/skills/doctl/other-services.md +56 -0
- package/plugins/doctl/skills/doctl/spec-management.md +74 -0
- package/plugins/git-github/.claude-plugin/plugin.json +8 -0
- package/plugins/git-github/skills/git-github/SKILL.md +85 -0
- package/plugins/git-github/skills/git-github/actions-debugging.md +102 -0
- package/plugins/git-github/skills/git-github/external-review.md +39 -0
- package/plugins/git-github/skills/git-github/getting-unstuck.md +106 -0
- package/plugins/git-github/skills/git-github/gh-api-recipes.md +73 -0
- package/plugins/git-github/skills/git-github/releases.md +53 -0
package/package.json
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "copier-template",
|
|
3
|
+
"description": "Maintain a Copier project template and propagate updates to generated repos: template anatomy, testing changes, releasing versions, and applying copier update in descendants with conflict resolution.",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "yeedle"
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: copier-template
|
|
3
|
+
description: Maintain a Copier project template and propagate updates to generated ("descendant") repos. Covers template anatomy (copier.yml, jinja, tasks), testing template changes, tagging/releasing versions, the automated update-notification PR pattern, and applying copier update in descendants with conflict resolution.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Copier Template Maintenance & Propagation
|
|
7
|
+
|
|
8
|
+
Patterns for the full lifecycle of a [Copier](https://copier.readthedocs.io/) project template: authoring changes, testing them, releasing versions, and rolling updates out to every repo generated from the template.
|
|
9
|
+
|
|
10
|
+
## When to Use This Skill
|
|
11
|
+
|
|
12
|
+
- Editing the template repo (questions, scaffold files, tasks, CI)
|
|
13
|
+
- "Upstream this pattern to the template" — porting something proven in a descendant
|
|
14
|
+
- Tagging/releasing a new template version
|
|
15
|
+
- Applying a template update in a descendant repo (often via an automated "template update available" PR)
|
|
16
|
+
- Debugging `copier copy`/`copier update` failures
|
|
17
|
+
|
|
18
|
+
## Mental Model
|
|
19
|
+
|
|
20
|
+
- The **template repo** holds `copier.yml` (questions + settings + tasks) and a `template/` subdirectory of scaffold files (some `.jinja`-suffixed for substitution). **Git tags (`v*`) are the version protocol**; GitHub Releases are the changelog protocol.
|
|
21
|
+
- Each **descendant** carries `.copier-answers.yml` recording its answers and `_commit: vX.Y.Z` — the template version it's on. Never hand-edit this file; `copier update` maintains it.
|
|
22
|
+
- `copier update` re-renders from old-tag → newest tag and three-way merges against local changes. **It always jumps to the latest tag** (unless `--vcs-ref` pins one) — a notification PR advertising v1.5.0 may actually land v1.8.0 if the template moved on.
|
|
23
|
+
- Run copier via `uvx copier ...` (no global install needed). Templates with `_tasks` require `--trust` — without it copier refuses to render at all. Non-interactive contexts also need `--defaults` (and `--data key=value` for required questions without defaults).
|
|
24
|
+
|
|
25
|
+
## Direction of Change
|
|
26
|
+
|
|
27
|
+
**Prove patterns in a real descendant first, then upstream.** Build and merge the feature in one generated project; once proven, port it into `template/` with jinja-aware adaptations. The upstream PR body should cite the originating repo/PR and include validation evidence (a project generated from the branch passing typecheck/lint/tests). Exception: infra-only changes (CI jobs, hooks config) can go straight to the template. For risky changes, stage the rollout — hand-run script in one repo first, graduate to the template once the win is proven.
|
|
28
|
+
|
|
29
|
+
When working in a descendant and a fix belongs in the template too: fix the symptom locally *and* make the corresponding change in the template repo (verify you have the right repo with `git remote -v` — don't trust the directory name).
|
|
30
|
+
|
|
31
|
+
## Releasing a Template Version
|
|
32
|
+
|
|
33
|
+
After merging to the template's main:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git checkout main && git pull --ff-only
|
|
37
|
+
git tag --sort=-v:refname | head -5 # see existing versions
|
|
38
|
+
git cat-file -t v<latest> # match the tag type convention (lightweight vs annotated)
|
|
39
|
+
git tag v<X.Y.Z> && git push origin v<X.Y.Z>
|
|
40
|
+
gh release create v<X.Y.Z> --title "v<X.Y.Z> — <summary>" --notes "$(cat <<'EOF'
|
|
41
|
+
## Changes
|
|
42
|
+
- ...
|
|
43
|
+
|
|
44
|
+
## Upgrading
|
|
45
|
+
Run `uvx copier update --trust --defaults` in your project.
|
|
46
|
+
|
|
47
|
+
Full diff: <template-repo-url>/compare/v<prev>...v<X.Y.Z>
|
|
48
|
+
EOF
|
|
49
|
+
)"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- Semver: patch = fixes/dep bumps; minor = new features, questions, or components; major = breaking structure changes.
|
|
53
|
+
- **Always create the GitHub Release, not just the tag.** Descendant notification PRs link to `/releases/tag/<version>` — a tag without a release produces dead links downstream.
|
|
54
|
+
|
|
55
|
+
## Automated Update Notification
|
|
56
|
+
|
|
57
|
+
The template ships its descendants a checker workflow (daily cron + `workflow_dispatch`) that compares `.copier-answers.yml`'s `_commit` against the template's highest remote tag:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
git ls-remote --tags --refs --sort=-v:refname <template-url> 'v*' | head -1
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
If newer, it pushes a **static branch name** (e.g. `chore/template-update`) with an `--allow-empty` commit and opens a PR whose body contains the version delta, release-notes/compare links, and step-by-step instructions an agent can execute. Hard-won details to keep if reimplementing: an explicit `permissions: contents: write, pull-requests: write` block (default token can't open PRs), a static branch name (dated branches caused duplicate PRs), and comparing **tag versions, not commit SHAs**.
|
|
64
|
+
|
|
65
|
+
## Further Reading
|
|
66
|
+
|
|
67
|
+
- **Template anatomy & testing changes**: [template-authoring.md](template-authoring.md)
|
|
68
|
+
- **Applying an update in a descendant** (the conflict-resolution procedure): [applying-updates.md](applying-updates.md)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Applying a Template Update in a Descendant
|
|
2
|
+
|
|
3
|
+
Usually triggered by the automated "template update available" PR — its body contains the runbook; this file is the full procedure with the judgment calls spelled out.
|
|
4
|
+
|
|
5
|
+
## The sequence
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
gh pr view <n> --json title,body # read the bot PR's instructions
|
|
9
|
+
git checkout chore/template-update && git pull # the bot's static branch
|
|
10
|
+
|
|
11
|
+
# clean tree required — copier refuses otherwise
|
|
12
|
+
git stash --include-untracked -m "wip before template update" # if dirty
|
|
13
|
+
|
|
14
|
+
uvx copier update --trust --defaults
|
|
15
|
+
|
|
16
|
+
# triage
|
|
17
|
+
git status --short # UU = unmerged (inline conflict markers)
|
|
18
|
+
find . -name '*.rej' # hunks copier couldn't apply
|
|
19
|
+
grep _commit .copier-answers.yml # confirm the new version
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Note the version it reports: `copier update` goes to the **latest** tag, which may be newer than the one the bot PR advertised. A multi-version jump means several releases' worth of changes land at once — budget for more conflicts.
|
|
23
|
+
|
|
24
|
+
## Resolving conflicts
|
|
25
|
+
|
|
26
|
+
Copier writes diff3-style inline markers:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
<<<<<<< before updating
|
|
30
|
+
<your project's current content>
|
|
31
|
+
||||||| last update
|
|
32
|
+
<what the old template version had>
|
|
33
|
+
=======
|
|
34
|
+
<what the new template version has>
|
|
35
|
+
>>>>>>> after updating
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Survey all conflict blocks first: `awk '/^<<<<<<< /,/^>>>>>>> /' <file>`.
|
|
39
|
+
|
|
40
|
+
**Decision procedure per file** — check `git log --oneline -- <file>`:
|
|
41
|
+
|
|
42
|
+
| File history | Resolution |
|
|
43
|
+
|---|---|
|
|
44
|
+
| Hand-customized in this project | Keep ours (project side) |
|
|
45
|
+
| Untouched scaffold since generation | Take theirs (template side) |
|
|
46
|
+
| Shared file with both kinds of changes (login page, CI workflow, app config) | Merge both — keep project customizations *and* add the template's new feature |
|
|
47
|
+
|
|
48
|
+
Mechanical resolutions with perl (whole-block operations, safe for multi-line):
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# keep ours: delete the entire conflict block (template side discarded)
|
|
52
|
+
perl -0pi -e 's/^<<<<<<< before updating\n(.*?)^\|\|\|\|\|\|\| last update\n.*?^>>>>>>> after updating\n/$1/gms' <file>
|
|
53
|
+
|
|
54
|
+
# keep both sides (ours then theirs)
|
|
55
|
+
perl -0pi -e 's/^<<<<<<< before updating\n(.*?)^\|\|\|\|\|\|\| last update\n.*?^=======\n(.*?)^>>>>>>> after updating\n/$1$2/gms' <file>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**`.rej` files**: copier couldn't apply a hunk (the local file diverged too far). Read the `.rej`, re-apply its *intent* manually — and check whether copier dropped project-specific content nearby (e.g. local vars in `.env.example`) — then `rm` the `.rej`.
|
|
59
|
+
|
|
60
|
+
**Review the non-conflicted changes too.** Copier silently overwrites scaffold-owned files, and a new template assumption can be wrong for this project (e.g. a type coercion that assumes numeric IDs in a project using string IDs). `git diff` every copier-touched app-code file; revert what doesn't fit, and consider whether the template itself needs a fix.
|
|
61
|
+
|
|
62
|
+
Final sweep before staging:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
grep -rn '<<<<<<<\|>>>>>>>' . --exclude-dir=node_modules
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Validate, commit, hand back
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
git add -A # includes .copier-answers.yml — it must be committed with the update
|
|
72
|
+
yarn install && yarn typecheck && yarn lint && yarn fmt:check && yarn test:run
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Commit as `chore: update to template vX.Y.Z` with a body listing the notable upstream changes **and each conflict resolution with its rationale** — that's the audit trail for the squash-merge. Then retitle the bot PR (`gh pr edit <n> --title "chore: update to template vX.Y.Z"`), push, watch CI, and ask the user before merging. The bot's empty placeholder commit is fine — it disappears in the squash.
|
|
76
|
+
|
|
77
|
+
**If something fails after the update**, prove whether it's pre-existing before blaming the update: `git worktree add /tmp/<proj>-main origin/main`, reuse node_modules (symlink), rerun the failing check there. A byte-identical failure on main means fix-forward in this PR, not a regression.
|
|
78
|
+
|
|
79
|
+
## Troubleshooting
|
|
80
|
+
|
|
81
|
+
| Symptom | Fix |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `Destination repository is dirty; cannot continue` | `git stash --include-untracked` — plain `git stash` misses untracked files (including editor swap files), which still count as dirty |
|
|
84
|
+
| Copier refuses to render at all | Template has `_tasks` — add `--trust` |
|
|
85
|
+
| Hangs or fails in non-interactive shells | Add `--defaults` (and `--data key=value` for questions without defaults) |
|
|
86
|
+
| Update landed a version you didn't expect | `copier update` always targets the latest tag; pin with `--vcs-ref v<X.Y.Z>` if you need a specific one |
|
|
87
|
+
| Template change is wrong for this project | Revert locally, note it in the commit body, open a template issue/PR if other descendants are affected |
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Template Anatomy & Testing Changes
|
|
2
|
+
|
|
3
|
+
## copier.yml settings
|
|
4
|
+
|
|
5
|
+
```yaml
|
|
6
|
+
_subdirectory: template # only this dir is rendered; repo root holds copier.yml, test.sh, README
|
|
7
|
+
_templates_suffix: .jinja # only .jinja files get substitution; everything else copies verbatim
|
|
8
|
+
_skip_if_exists:
|
|
9
|
+
- ".env" # never clobber these on update
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Questions
|
|
13
|
+
|
|
14
|
+
```yaml
|
|
15
|
+
project_name:
|
|
16
|
+
type: str
|
|
17
|
+
validator: "{% if not project_name %}Required{% endif %}"
|
|
18
|
+
database_name:
|
|
19
|
+
type: str
|
|
20
|
+
default: "{{ project_name }}" # defaults can reference earlier answers
|
|
21
|
+
include_ci:
|
|
22
|
+
type: bool
|
|
23
|
+
default: true
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Feature toggles as `include_*` booleans defaulting `true` keep `copier copy --defaults` producing a fully-featured project.
|
|
27
|
+
|
|
28
|
+
## Conditional files via filename templating
|
|
29
|
+
|
|
30
|
+
A file named `{% if include_ci %}ci.yml{% endif %}.jinja` renders to `ci.yml` when the flag is true and to an empty filename (skipped) when false. This works for plain files too — the filename is always templated, only contents need the `.jinja` suffix.
|
|
31
|
+
|
|
32
|
+
## The self-rendering answers file
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
template/{{_copier_conf.answers_file}}.jinja
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
containing:
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
|
|
42
|
+
{{ _copier_answers|to_nice_yaml }}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This is what writes `.copier-answers.yml` into every descendant.
|
|
46
|
+
|
|
47
|
+
## Tasks
|
|
48
|
+
|
|
49
|
+
Gate scaffold-time tasks so they run on first copy only, never on update:
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
_tasks:
|
|
53
|
+
- command: createdb {{ database_name }}
|
|
54
|
+
when: "{{ _copier_operation == 'copy' }}"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- **Task order is load-bearing**: anything importing generated artifacts must run after the generator (e.g. seed after codegen); `git init` before installing git hooks.
|
|
58
|
+
- Tasks needing env vars: `bash -c 'set -a && source .env && set +a && <cmd>'`.
|
|
59
|
+
- Tasks gated on a question flag: combine conditions in `when`.
|
|
60
|
+
- Prefer post-gen install tasks (e.g. `npx <tool> add ...`) over vendoring third-party files into the template — vendored copies go stale.
|
|
61
|
+
|
|
62
|
+
## Escaping `${{ }}` in templated GitHub workflows
|
|
63
|
+
|
|
64
|
+
Jinja eats GitHub Actions expressions in `.jinja` workflow files. Two working escapes:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
{% raw %}${{ secrets.GITHUB_TOKEN }}{% endraw %}
|
|
68
|
+
${{ '{{' }} secrets.GITHUB_TOKEN {{ '}}' }}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`{% raw %}` blocks are cleaner when a whole region is Actions syntax; the inline form suits one-offs inside otherwise-templated lines.
|
|
72
|
+
|
|
73
|
+
## Testing template changes
|
|
74
|
+
|
|
75
|
+
A `test.sh` that generates a real project and runs its full gate:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
tmp=$(mktemp -d)
|
|
79
|
+
trap 'dropdb --if-exists <test-dbs>; rm -rf "$tmp"' EXIT
|
|
80
|
+
uvx copier copy --trust --defaults --vcs-ref HEAD \
|
|
81
|
+
--data project_name=smoke-test --data project_description=test \
|
|
82
|
+
. "$tmp"
|
|
83
|
+
cd "$tmp" && yarn install && yarn lint && yarn fmt:check && yarn test:run
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- **`--vcs-ref HEAD` tests committed HEAD instead of the last tag** — without it, copier renders the latest *tag* and your changes are silently absent.
|
|
87
|
+
- For *uncommitted* working-tree validation: `cp -r` the template to a temp dir, `rm -rf .git` in the copy, and `copier copy` from there.
|
|
88
|
+
- When the generated project reveals a bug, **fix it in `template/` source and re-render** — never only in the generated copy. Re-sync (`cp` the fixed file into the generated project) to re-verify without a full regeneration.
|
|
89
|
+
- Generating a project is also how template-level type/lint bugs get caught — run the descendant's typecheck against the freshly generated output as part of any nontrivial template PR, and say so in the PR body.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doctl",
|
|
3
|
-
"description": "Manage DigitalOcean
|
|
4
|
-
"version": "1.
|
|
3
|
+
"description": "Manage DigitalOcean resources with the doctl CLI: auth contexts, App Platform deployments and logs, app specs and secrets, repo-to-app resolution, databases, Spaces, and droplets.",
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "yeedle"
|
|
7
7
|
}
|
|
@@ -1,93 +1,119 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: doctl
|
|
3
|
-
description: Manage DigitalOcean
|
|
3
|
+
description: Manage DigitalOcean resources with the doctl CLI. Covers auth contexts, App Platform (deployments, logs, env vars/secrets via app specs), mapping a git repo to its app, managed databases, Spaces keys, and droplets.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# DigitalOcean doctl CLI Patterns
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Patterns for managing DigitalOcean resources via the `doctl` CLI, centered on App Platform.
|
|
9
9
|
|
|
10
10
|
## When to Use This Skill
|
|
11
11
|
|
|
12
12
|
Use this skill when:
|
|
13
13
|
- Deploying or monitoring apps on DigitalOcean App Platform
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
14
|
+
- Checking deployment status, build failures, or runtime logs
|
|
15
|
+
- Adding/changing env vars or secrets on a deployed app
|
|
16
|
+
- Working with managed databases, Spaces, or droplets
|
|
17
|
+
- Figuring out which DO account/app corresponds to the current repo
|
|
17
18
|
|
|
18
19
|
## Auth Contexts
|
|
19
20
|
|
|
20
21
|
doctl supports named auth contexts for managing multiple accounts/teams.
|
|
21
22
|
|
|
22
23
|
```bash
|
|
23
|
-
#
|
|
24
|
-
doctl
|
|
24
|
+
doctl auth list # list contexts; (current) marks the active one
|
|
25
|
+
doctl account get --context <name> # cheap probe: is this context valid, which account is it?
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Prefer the `--context` flag over switching.** Every doctl command accepts `--context <name>` (before or after the subcommand). This targets one account for one command without mutating global state — important when a session touches multiple accounts:
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
doctl
|
|
30
|
+
```bash
|
|
31
|
+
doctl apps list --context <ctx>
|
|
32
|
+
for ctx in $(doctl auth list); do doctl apps list --context "$ctx"; done
|
|
28
33
|
```
|
|
29
34
|
|
|
30
|
-
|
|
35
|
+
Only use `doctl auth switch --context <name>` when the user explicitly wants the default changed.
|
|
36
|
+
|
|
37
|
+
**`doctl auth init --context <name>` is interactive** (it prompts for a pasted token) — an agent cannot complete it. Ask the user to run it themselves.
|
|
31
38
|
|
|
32
|
-
## App
|
|
39
|
+
## Resolving the Current Repo to a Context + App
|
|
33
40
|
|
|
34
|
-
|
|
41
|
+
App specs embed their source repo, so "check prod logs" is answerable from inside any repo:
|
|
35
42
|
|
|
36
43
|
```bash
|
|
37
|
-
|
|
38
|
-
doctl
|
|
44
|
+
repo=$(git remote get-url origin | sed -E 's#.*github.com[:/]##; s#\.git$##')
|
|
45
|
+
for ctx in $(doctl auth list); do
|
|
46
|
+
doctl apps list --context "$ctx" -o json 2>/dev/null | jq -r --arg ctx "$ctx" --arg repo "$repo" \
|
|
47
|
+
'.[] | select([.spec.services[]?, .spec.static_sites[]?, .spec.workers[]?, .spec.jobs[]?]
|
|
48
|
+
| any(.github.repo == $repo)) | "\($ctx)\t\(.spec.name)\t\(.id)"'
|
|
49
|
+
done
|
|
39
50
|
```
|
|
40
51
|
|
|
41
|
-
|
|
52
|
+
This costs one API call per context — resolve once per session and reuse the `(context, app-id)` pair. If a repo backs multiple apps (e.g. staging + prod, or one repo deployed to several accounts), list the matches and ask the user which one they mean. Apps deployed without a git source won't be found this way.
|
|
42
53
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
### Monitoring Deployments
|
|
54
|
+
## App Platform Basics
|
|
46
55
|
|
|
47
56
|
```bash
|
|
48
|
-
#
|
|
49
|
-
doctl apps list-deployments <app-id>
|
|
57
|
+
doctl apps list --context <ctx> # ID, Spec.Name, DefaultIngress, deployment IDs
|
|
58
|
+
doctl apps list-deployments <app-id> # recent deployments: ID, Cause, Progress, Phase
|
|
59
|
+
doctl apps get <app-id> --format DefaultIngress,ActiveDeployment.Phase,InProgressDeployment.ID
|
|
60
|
+
doctl apps list-domains <app-id> # custom domains (there is no Domains column)
|
|
50
61
|
```
|
|
51
62
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Deployment phases:
|
|
55
|
-
- `PENDING_BUILD` — queued
|
|
56
|
-
- `BUILDING` — build in progress
|
|
57
|
-
- `DEPLOYING` — deploying built artifacts
|
|
58
|
-
- `ACTIVE` — successfully deployed and serving traffic
|
|
59
|
-
- `SUPERSEDED` — replaced by a newer deployment
|
|
60
|
-
- `ERROR` — deployment failed
|
|
63
|
+
Most commands require the app UUID, not the name — get it from `doctl apps list`.
|
|
61
64
|
|
|
62
|
-
The `Cause` column
|
|
65
|
+
The `Cause` column tells you *why* a deployment happened: `commit <sha> pushed to <repo>` for git pushes vs `app spec updated` for config changes.
|
|
63
66
|
|
|
64
|
-
|
|
67
|
+
Deployment phases: `PENDING_BUILD` → `BUILDING` → `DEPLOYING` → `ACTIVE`. Terminal failure states: `ERROR`, `CANCELED`. `SUPERSEDED` means replaced by a newer deployment.
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
# Get build logs for a specific deployment
|
|
68
|
-
doctl apps logs <app-id> --deployment <deployment-id> --type build
|
|
69
|
+
### Deploying
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
doctl apps logs <app-id> --type run
|
|
71
|
+
Apps connected to GitHub auto-deploy on push to the configured branch. To redeploy without a code change (config refresh, transient build failure):
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
doctl apps
|
|
73
|
+
```bash
|
|
74
|
+
doctl apps create-deployment <app-id> --format ID,Phase
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
### Waiting for a deployment
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
Poll bounded, with a fixed or escalating interval — never an unbounded `--follow`/watch in an agent context:
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
|
-
|
|
83
|
-
doctl apps get <app-id>
|
|
82
|
+
for i in $(seq 1 90); do
|
|
83
|
+
PHASE=$(doctl apps get-deployment <app-id> <deployment-id> --format Phase --no-header | tr -d ' ')
|
|
84
|
+
case "$PHASE" in
|
|
85
|
+
ACTIVE) echo deployed; break;;
|
|
86
|
+
ERROR|CANCELED|SUPERSEDED) echo "failed: $PHASE"; exit 1;;
|
|
87
|
+
esac
|
|
88
|
+
sleep 20
|
|
89
|
+
done
|
|
90
|
+
```
|
|
84
91
|
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
## Logs
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
doctl apps logs <app-id> --type run --tail 500 # bounded runtime logs
|
|
96
|
+
doctl apps logs <app-id> <component> --type build # component is a POSITIONAL arg, not a flag
|
|
97
|
+
doctl apps logs <app-id> --type run --follow # live tail (interactive use only)
|
|
87
98
|
```
|
|
88
99
|
|
|
89
|
-
|
|
100
|
+
- Log types: `build`, `deploy`, `run` (default). There is no `--component` flag — pass the component name as the second positional argument.
|
|
101
|
+
- Always use `--tail N` and grep (`| grep -iE 'error|timeout|oom'`) rather than dumping everything — run logs can contain live secrets.
|
|
102
|
+
- **Logs rotate on each deployment and retention is short.** Yesterday's crash logs are usually gone after today's deploy (unless log forwarding is configured). Capture logs immediately after triggering the thing you're observing.
|
|
103
|
+
- Run logs are only retrievable from the **active** deployment — `--deployment <old-id> --type run` fails with a 400 (`phase final_cleanup`). Build logs of older deployments are fine.
|
|
104
|
+
- A brand-new app has no logs until its first deployment starts (`no deployment found for app`).
|
|
105
|
+
- To tell a crash-restart from a deploy-restart, compare timestamps against `list-deployments` Created times.
|
|
106
|
+
|
|
107
|
+
## Output Formats: `--format`, `-o json`
|
|
108
|
+
|
|
109
|
+
- **Column names differ per subcommand** and doctl version. `apps list --format ActiveDeployment.Phase` fails (`unknown column`) — but the same column works on `apps get`. Nested names use dots (`Spec.Name`, not `SpecName`). On `unknown column`, fall back to `-o json | jq` rather than guessing.
|
|
110
|
+
- **`-o json` returns an array even for a single resource** — use `.[0].spec...`, not `.spec...`.
|
|
111
|
+
- **`apps spec get` always emits YAML** — it silently ignores `-o json`. Don't pipe it to a JSON parser.
|
|
112
|
+
- **A bad `--format` on a mutating command does NOT roll back the mutation.** `doctl apps update ... --format BadColumn` applies the update, then errors. Do not re-run the command — verify state instead.
|
|
113
|
+
- Empty fields render as the literal string `<nil>`; `--no-header` keeps column padding — `tr -d ' '` before string-comparing.
|
|
114
|
+
- `--http-retry-max` (global flag) auto-retries 429/5xx responses.
|
|
115
|
+
|
|
116
|
+
## Further Reading
|
|
90
117
|
|
|
91
|
-
- **
|
|
92
|
-
- **
|
|
93
|
-
- **App ID vs Name**: Most commands require the app UUID, not the human-readable name. Get it from `doctl apps list`.
|
|
118
|
+
- **App specs — env vars, secrets, creating apps**: see [spec-management.md](spec-management.md)
|
|
119
|
+
- **Databases, Spaces, droplets, DNS**: see [other-services.md](other-services.md)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Databases, Spaces, Droplets, DNS
|
|
2
|
+
|
|
3
|
+
## Managed Databases
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
doctl databases list --format ID,Name,Engine,Version,NumNodes,Size
|
|
7
|
+
doctl databases connection <db-id> --format URI --no-header # full credentialed URI
|
|
8
|
+
doctl databases ca <db-id> # cluster CA certificate
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- **The connection URI contains live credentials** — treat command output as a secret. Don't echo it into logs; pipe it directly to where it's needed.
|
|
12
|
+
- **Check `Version`** and keep CI/local database versions in sync with production — a test suite running `postgres:15` against a pg-18 production cluster hides version-specific behavior.
|
|
13
|
+
- Connections use port 25060 with `sslmode=require`. **TLS trap**: some clients (e.g. newer `pg-connection-string`) silently upgrade `require` to `verify-full`, which rejects DO's CA under the default trust store. Fix: supply the CA from `doctl databases ca <db-id>` explicitly, or configure ssl options in code rather than relying on the URI.
|
|
14
|
+
|
|
15
|
+
## Spaces
|
|
16
|
+
|
|
17
|
+
**`doctl spaces` manages access keys only — not buckets.** Create and manage buckets with the `aws` CLI (or s3cmd) against the Spaces endpoint:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
aws s3 mb s3://<bucket> --endpoint-url https://<region>.digitaloceanspaces.com
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Keys:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
doctl spaces keys create <name> --grants 'bucket=<bucket>;permission=readwrite' -o json > /tmp/key.json
|
|
27
|
+
doctl spaces keys list
|
|
28
|
+
doctl spaces keys delete <ACCESS_KEY> # no --force flag; prompts — use `yes |` when non-interactive
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- Grant permissions: `read`, `readwrite`, `fullaccess`. An empty `bucket=` grants all buckets.
|
|
32
|
+
- **The secret key is shown exactly once, at creation.** Capture it to a temp file with `-o json`, never print it, and delete the file after storing it where it belongs.
|
|
33
|
+
- Unlike most doctl delete commands, `spaces keys delete` has no `--force` flag (it errors `unknown flag`).
|
|
34
|
+
- Bootstrap pattern: create a temporary `fullaccess` key to create the bucket, mint a bucket-scoped `readwrite` key for the app, swap it in, then delete the full-access key.
|
|
35
|
+
|
|
36
|
+
## Droplets
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
doctl compute ssh-key list # find key IDs (match yours via ssh-keygen -l -E md5)
|
|
40
|
+
doctl compute droplet create <name> --region <region> --size s-1vcpu-1gb \
|
|
41
|
+
--image ubuntu-24-04-x64 --ssh-keys <key-id> --enable-monitoring --wait
|
|
42
|
+
doctl compute droplet list --format Name,PublicIPv4,Status
|
|
43
|
+
doctl compute ssh <droplet-name> # ssh by name (interactive)
|
|
44
|
+
doctl compute firewall list --format Name,InboundRules,DropletIDs
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`--wait` blocks until the droplet is active, so the IP is immediately available from `droplet list`. For non-interactive remote commands prefer plain ssh: `ssh -o StrictHostKeyChecking=accept-new root@<ip> "<cmd>"`.
|
|
48
|
+
|
|
49
|
+
## DNS
|
|
50
|
+
|
|
51
|
+
Domain commands live under `compute` — `doctl domains ...` fails with `unknown command`:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
doctl compute domain list
|
|
55
|
+
doctl compute domain records list <domain>
|
|
56
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# App Specs: Env Vars, Secrets, Creating Apps
|
|
2
|
+
|
|
3
|
+
The app spec is the single source of truth for an App Platform app's configuration (components, env vars, routes, instance sizes). Most config changes are a GET → edit → PUT round-trip.
|
|
4
|
+
|
|
5
|
+
## Changing Env Vars / Secrets (the standard workflow)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
doctl apps spec get <app-id> > /tmp/spec.yaml
|
|
9
|
+
# edit /tmp/spec.yaml — add under the relevant service's envs:
|
|
10
|
+
# - key: MY_SECRET
|
|
11
|
+
# scope: RUN_AND_BUILD_TIME
|
|
12
|
+
# type: SECRET
|
|
13
|
+
# value: <plaintext>
|
|
14
|
+
doctl apps update <app-id> --spec /tmp/spec.yaml
|
|
15
|
+
rm /tmp/spec.yaml # the temp file held plaintext secrets
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Facts that matter:
|
|
19
|
+
|
|
20
|
+
- **`type: SECRET` values are submitted as plaintext and encrypted on ingest.** They read back as `EV[1:...]` blobs and can never be read back in plaintext via doctl.
|
|
21
|
+
- **Existing `EV[...]` blobs survive the round-trip unchanged** — you do not need to re-supply secret values when editing other parts of the spec.
|
|
22
|
+
- **DO encrypts whatever literal string you submit.** A placeholder like `VALUE_TO_SET` gets encrypted and deployed as the real value. Never put placeholder text in a SECRET value — keep the `EV[...]` blob or paste the real plaintext.
|
|
23
|
+
- **`apps update --spec` triggers a new deployment** (Cause: `app spec updated`). The update command's own output may show a blank `In Progress Deployment ID` even though a deployment was created — confirm with `doctl apps list-deployments <app-id> | head -3`.
|
|
24
|
+
- To verify a secret landed: `doctl apps spec get <app-id> | grep -A3 MY_SECRET` (expect an `EV[...]` value).
|
|
25
|
+
- To inspect env config without the YAML: `doctl apps get <app-id> -o json | jq '.[0].spec.services[0].envs[] | {key, scope, type}'` (note the `.[0]` — json output is an array).
|
|
26
|
+
- The only way to read a runtime env value is a console session into the running container — see "apps console" below.
|
|
27
|
+
|
|
28
|
+
Env `scope` values: `RUN_TIME`, `BUILD_TIME`, `RUN_AND_BUILD_TIME`. Env vars can live at the app level (shared) or per-component.
|
|
29
|
+
|
|
30
|
+
## Validating Specs: `--schema-only` for Update Specs
|
|
31
|
+
|
|
32
|
+
`doctl apps spec validate spec.yaml` calls the propose endpoint, which simulates app **creation**. A spec pulled from a live app fails validation with:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
secret env value must not be encrypted before app is created
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This is a false alarm — `apps update` accepts `EV[...]` values fine. To pre-validate a spec destined for `apps update`, use:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
doctl apps spec validate spec.yaml --schema-only
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Full (non-schema-only) validation is still useful for specs that will be passed to `apps create`.
|
|
45
|
+
|
|
46
|
+
## Creating an App
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
doctl apps create --spec .do/app.yaml --context <ctx> [--project-id <project-uuid>]
|
|
50
|
+
doctl projects list --format ID,Name # to find the project ID
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- Convention: keep a sanitized spec in the repo (e.g. `.do/app.yaml`) with SECRET keys listed but values empty; inject real values into a temp copy at create time and delete it after.
|
|
54
|
+
- Specs can also be piped inline: `doctl apps create --spec - <<'EOF' ... EOF` (same for `update`).
|
|
55
|
+
- **GitHub-access 400**: `POST /v2/apps: 400 ... GitHub user does not have access to <org>/<repo>` means the DigitalOcean GitHub App isn't installed/authorized on that org. This is fixed in the browser (GitHub → Settings → Applications), not via doctl — hand it to the user, then retry the create.
|
|
56
|
+
- `DefaultIngress` is empty (`<nil>`) until the first deployment goes ACTIVE. Fetch it afterward: `doctl apps get <app-id> --format DefaultIngress --no-header`.
|
|
57
|
+
- No logs exist until the first deployment starts.
|
|
58
|
+
|
|
59
|
+
## Instance Sizes / Pricing
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
doctl apps tier instance-size list
|
|
63
|
+
doctl apps tier instance-size list -o json | jq '[.[] | {slug, monthly: .usd_per_month_cost}] | sort_by(.monthly)'
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Use the slug in the spec's `instance_size_slug`.
|
|
67
|
+
|
|
68
|
+
## apps console — Interactive Only
|
|
69
|
+
|
|
70
|
+
`doctl apps console <app-id> <component>` opens an ephemeral shell in a running component — the only way to read live secret values or poke the runtime environment. But:
|
|
71
|
+
|
|
72
|
+
- There is **no `--command` flag**, and piping stdin fails (`error setting terminal to raw mode: inappropriate ioctl for device`). It requires a real TTY.
|
|
73
|
+
- An agent cannot drive it. Hand the user the exact command to run themselves, or reproduce the container environment locally with `docker run` instead.
|
|
74
|
+
- Instances are ephemeral — nothing done in a console session persists.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "git-github",
|
|
3
|
+
"description": "Git and GitHub (gh CLI) workflows for agents: the branch-to-PR loop, reading PR/CI state, debugging failed Actions runs, repair ladders for stuck git states, gh api recipes, and release flows.",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "yeedle"
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: git-github
|
|
3
|
+
description: Git and GitHub (gh CLI) workflows for agents - the branch-to-PR loop, reading PR and CI state, debugging failed GitHub Actions runs, getting unstuck from rejected pushes and rebase messes, gh api recipes, and release flows.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Git + GitHub Workflows
|
|
7
|
+
|
|
8
|
+
Battle-tested git and `gh` patterns for working in repos as an agent: the everyday branch→PR loop, interrogating PR/CI state, and recovering from the states git gets itself into.
|
|
9
|
+
|
|
10
|
+
## Ground Rules
|
|
11
|
+
|
|
12
|
+
- **Never push to the default branch unless the user explicitly says to.** "Commit and push" means the current branch. If on the default branch, branch first.
|
|
13
|
+
- **Destructive operations require explicit user authorization**: `push --force` (even with lease, outside your own just-rebased branch), deleting remote branches, closing PRs you didn't open, `reset --hard`, `--no-verify`.
|
|
14
|
+
- **Quote pathspecs containing brackets.** zsh globs `[id].get.ts` into `no matches found` — write `git add 'server/api/[id].get.ts'`. This bites on every bracketed-route codebase.
|
|
15
|
+
- **Prefer `git -C <path>`** over `cd <path> && git ...` — compound cd commands trigger permission prompts and reset the shell cwd.
|
|
16
|
+
- **Branch naming**: follow the repo's convention (`feat/`, `fix/`, `chore/`, `ci/`). If an issue tracker (e.g. Linear) suggests a branch name for the ticket, use it verbatim — it powers the tracker↔GitHub integration (auto-close on merge).
|
|
17
|
+
- **Bounded polling, never unbounded watching.** `gh run watch` / `--watch` can die on network timeouts mid-wait; in agent contexts prefer a bounded loop with escalating sleeps (30/60/90s).
|
|
18
|
+
|
|
19
|
+
## The Branch → PR Loop
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# 1. Start from fresh main
|
|
23
|
+
git checkout main && git pull --ff-only
|
|
24
|
+
git checkout -b feat/<short-description>
|
|
25
|
+
|
|
26
|
+
# 2. Commit with a heredoc (multi-line messages survive quoting)
|
|
27
|
+
git commit -m "$(cat <<'EOF'
|
|
28
|
+
feat(scope): one-line summary
|
|
29
|
+
|
|
30
|
+
Why this change exists, not just what it does.
|
|
31
|
+
EOF
|
|
32
|
+
)"
|
|
33
|
+
|
|
34
|
+
# 3. Push and open the PR with a structured body
|
|
35
|
+
git push -u origin feat/<short-description>
|
|
36
|
+
gh pr create --title "feat(scope): one-line summary" --body "$(cat <<'EOF'
|
|
37
|
+
## What
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
## Why
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
## Notes for reviewers
|
|
44
|
+
...
|
|
45
|
+
EOF
|
|
46
|
+
)"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- **Keep the PR description current.** After material scope changes, `gh pr edit <n> --body "$(cat <<'EOF' ... EOF)"`.
|
|
50
|
+
- Merge style: `gh pr merge <n> --squash --delete-branch`; verify with `gh pr view <n> --json state,mergedAt`.
|
|
51
|
+
- After merge: `git switch main && git pull --ff-only`, clean up `[gone]` branches, start the next branch from fresh main.
|
|
52
|
+
- One concern per PR — hotfixes and review findings go in separate PRs unless told otherwise.
|
|
53
|
+
- Stacked PRs: `gh pr create --base <parent-branch>`; after the parent merges, retarget with `gh pr edit <n> --base main` (and see [getting-unstuck.md](getting-unstuck.md) for rebasing onto main after the parent was squash-merged).
|
|
54
|
+
|
|
55
|
+
## Reading PR and CI State
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
gh pr view <n> --json mergeable,mergeStateStatus,reviewDecision,state,mergedAt
|
|
59
|
+
gh pr view <n> --json body -q .body # read the current description
|
|
60
|
+
gh pr diff <n> # the review workhorse; --name-only for the file list
|
|
61
|
+
gh pr checks <n> # CI status table
|
|
62
|
+
gh pr checks <n> --json name,bucket,link --jq '.[] | select(.bucket=="fail")'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- `mergeable: CONFLICTING` / `mergeStateStatus: DIRTY` → branch conflicts with base; `BEHIND` → needs update.
|
|
66
|
+
- Bounded CI wait: `until gh pr checks <n> 2>&1 | grep -qvE 'pending'; do sleep 30; done` — or check, sleep 30/60/90, re-check.
|
|
67
|
+
|
|
68
|
+
## Pre-push Hook Noise
|
|
69
|
+
|
|
70
|
+
When a push fails inside a compound command (or behind lefthook/husky pre-push hooks), the hook's lint/test output drowns the real git error. Isolate it:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git push -u origin <branch> > /tmp/push.log 2>&1; echo "exit=$?"
|
|
74
|
+
grep -E '! \[reject|error:|fatal:' /tmp/push.log
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Do not bypass failing hooks with `--no-verify` unless the user says to.
|
|
78
|
+
|
|
79
|
+
## Further Reading
|
|
80
|
+
|
|
81
|
+
- **Debugging failed Actions runs** (the full playbook): [actions-debugging.md](actions-debugging.md)
|
|
82
|
+
- **Repair ladders** — rejected pushes, blocked checkouts, rebase/conflict recovery, shallow clones, worktrees: [getting-unstuck.md](getting-unstuck.md)
|
|
83
|
+
- **gh api recipes** — PR comments, reading files without checkout, repo settings, PAT gotchas: [gh-api-recipes.md](gh-api-recipes.md)
|
|
84
|
+
- **Releases & publishing** — tags, gh release, npm Trusted Publishing, release-please: [releases.md](releases.md)
|
|
85
|
+
- **External review loop** — using the codex CLI as an adversarial pre-merge reviewer: [external-review.md](external-review.md)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Debugging Failed GitHub Actions Runs
|
|
2
|
+
|
|
3
|
+
The playbook, in the order that actually works.
|
|
4
|
+
|
|
5
|
+
## 1. Start from the PR, not the run
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
gh pr checks <n> --watch --interval 20 --fail-fast=false # interactive sessions
|
|
9
|
+
gh pr checks <n> # one-shot snapshot for poll loops
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The output includes run/job links with the IDs you need. `--required` limits to required checks.
|
|
13
|
+
|
|
14
|
+
For branch pushes without a PR, find the run:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
rid=$(gh run list --branch <branch> --limit 1 --json databaseId,status,conclusion --jq '.[0].databaseId')
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 2. Get the failing-step logs
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
gh run view <run-id> --log-failed 2>&1 | tail -50
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
This is the first command of every failure investigation. When noisy, narrow it:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
gh run view <run-id> --log-failed | grep -E 'FAIL|error|✗'
|
|
30
|
+
# aggregate TypeScript errors by code:
|
|
31
|
+
gh run view <run-id> --log-failed | grep -oE 'error TS[0-9]+' | sort | uniq -c | sort -rn
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Runner log lines are prefixed with `job\tstep\ttimestamp` — strip before aggregating:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
sed -E 's/^[^\t]*\t[^\t]*\t[0-9T:.Z-]* //'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
and file paths are absolute on the runner — strip `s#^/home/runner/work/<repo>/<repo>/##` to get repo-relative paths.
|
|
41
|
+
|
|
42
|
+
## 3. Need context before the failure?
|
|
43
|
+
|
|
44
|
+
`--log-failed` shows only the failing step. For surrounding context:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
gh run view <run-id> --json status,conclusion,jobs --jq '{status, conclusion, jobs: [.jobs[] | {name, status, conclusion}]}'
|
|
48
|
+
gh run view <run-id> --job <job-id> --log | grep -E '<pipeline markers>'
|
|
49
|
+
gh api repos/<owner>/<repo>/actions/jobs/<job-id>/logs | tail -60 # raw dump, last resort
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 4. Flaky or real? Check main
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
gh run list --branch main --limit 3 --json databaseId,status,conclusion
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If main is red with the same failure, the problem isn't your branch. If it looks like a flake:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
gh run rerun <run-id> --failed # reruns only the failed jobs
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Note: `gh run rerun --job <id>` only works on failed jobs within the retention window ("job cannot be rerun" otherwise).
|
|
65
|
+
|
|
66
|
+
## 5. Reproduce locally before fixing
|
|
67
|
+
|
|
68
|
+
Run the exact failing command from the workflow (`yarn test:run -- <file>`, `yarn fmt:check`, etc.). For environment-dependent failures (Linux vs macOS differences, missing gitignored fixtures, float-precision diffs in generated output), reproduce inside the CI image:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
docker run --rm -v "$PWD":/work -w /work node:22 bash -c \
|
|
72
|
+
'yarn install --immutable && <failing command>'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 6. Fix → push → re-watch → merge
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
git push
|
|
79
|
+
sleep 30 && gh pr checks <n> # then 60s, 90s — escalating, bounded
|
|
80
|
+
gh pr merge <n> --squash --delete-branch
|
|
81
|
+
gh pr view <n> --json state,mergedAt
|
|
82
|
+
git switch main && git pull --ff-only
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Watching: bounded polls beat unbounded watches
|
|
86
|
+
|
|
87
|
+
`gh run watch <id> --exit-status` is convenient but long watches can die with a GraphQL network timeout, losing the wait entirely. In agent contexts prefer:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
for s in 30 60 90 90 90; do
|
|
91
|
+
sleep $s
|
|
92
|
+
state=$(gh run view <run-id> --json status,conclusion --jq '"\(.status)/\(.conclusion)"')
|
|
93
|
+
echo "$state"; [[ "$state" == completed/* ]] && break
|
|
94
|
+
done
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Common root causes (in observed frequency order)
|
|
98
|
+
|
|
99
|
+
1. **Formatter/lint check failures** — fix is `yarn fmt` (or the repo's equivalent), commit, push.
|
|
100
|
+
2. **Test-only bugs** — assumptions that break under the CI harness (e.g. transaction-rollback test isolation).
|
|
101
|
+
3. **Environment differences** — CI Linux vs local macOS: gitignored fixture files missing in CI, locale/precision output diffs.
|
|
102
|
+
4. **Genuine flakes** — rerun `--failed`; if it recurs, it's not a flake.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# External Review Loop (codex as adversarial reviewer)
|
|
2
|
+
|
|
3
|
+
Use OpenAI's `codex` CLI as an independent reviewer of your own work before a human sees it. The loop: review → triage → fix fair findings → commit → re-review, until clean.
|
|
4
|
+
|
|
5
|
+
## Invocation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
codex review --base main 2>&1 | tail -120 # review branch vs base (the default move)
|
|
9
|
+
codex review --base origin/main ... # when local main may be stale
|
|
10
|
+
codex review --uncommitted ... # working-tree changes, pre-commit
|
|
11
|
+
codex review --commit <sha> ... # single commit
|
|
12
|
+
codex review <PR-number> # codex reads the PR description for intent
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- Steer with a positional prompt: `codex review --base main "Focus on X, Y. Report only issues genuinely worth fixing."` For long briefs use `"$(cat /tmp/review-prompt.md)"` or stdin via `-` (with `--title` for display context). A good brief states the feature context, prioritized focus areas (security first), what to skip (style/lint), and a mandated output format ("P0/P1/P2 punch list, file:line per finding").
|
|
16
|
+
- Reviews take minutes. Run in the background redirecting to a file (`codex review --base main > /tmp/review-r1.txt 2>&1`), then read the file when the process exits.
|
|
17
|
+
- **Exit code is 0 even with findings** — judge clean/dirty from the text, not `$?`.
|
|
18
|
+
- Print `git branch --show-current` and `git rev-parse --short HEAD` around the review so it's unambiguous which state was reviewed — stale-state false positives (reviewing before a push/amend landed) are the most common confusion.
|
|
19
|
+
- Older CLI versions reject `--base` combined with a prompt (`cannot be used with '[PROMPT]'`); pass the prompt alone, use stdin `-`, or upgrade.
|
|
20
|
+
|
|
21
|
+
## The loop
|
|
22
|
+
|
|
23
|
+
1. Commit and push the work, then run the review.
|
|
24
|
+
2. **Triage every finding before touching code.** Codex emits `[P1]/[P2]/[P3] — file:line` with rationale. Verify each at the cited location, then classify: fair (fix it), stale (already fixed, or presupposes old state — say so), or judgment call (present to the user with a recommendation). Never silently drop a finding — rebut it explicitly.
|
|
25
|
+
3. Fix the fair ones; keep provenance in the commit message: `(codex review, P2)` or `fix: address codex review round 3`.
|
|
26
|
+
4. Push and re-review. Small PRs converge in 1–3 rounds; large or security-sensitive features can take 6+.
|
|
27
|
+
5. **Brief later rounds.** List prior findings and their fixes ("don't re-flag these"), point fresh eyes at not-yet-audited surfaces, and demand a verdict: "P0/P1 only, skip nitpicks — or say plainly: no issues, ship it."
|
|
28
|
+
6. Once correctness is clean, optionally flip the lens for one final pass: "Do NOT look for bugs — those have been reviewed exhaustively. Review ONLY for over-engineering and simplification opportunities." Present those findings; don't auto-implement them.
|
|
29
|
+
|
|
30
|
+
## Conventions
|
|
31
|
+
|
|
32
|
+
- Record the outcome in the PR body's verification section: `codex review --base main: clean (after iterating on N findings — ...)`.
|
|
33
|
+
- Findings that arrive after the PR merged go in a **follow-up PR** referencing the original — never amend a merged branch.
|
|
34
|
+
- The user arbitrates dismissals of borderline findings.
|
|
35
|
+
- To run the loop unattended, set a session goal (Stop hook), e.g.: "run `codex review` on this PR and fix any finding you judge important. Repeat until there are no findings that need to be addressed." **Phrase the escape clause carefully** — state explicitly whose judgment ends the loop ("findings *the assistant* deems dismissible"). An ambiguous "or you think they don't need addressing" can wedge the hook: the checker may read "you" as the user, so the agent's own dismissal never satisfies the goal.
|
|
36
|
+
|
|
37
|
+
## Sibling pattern: plan review
|
|
38
|
+
|
|
39
|
+
The same adversarial-reviewer move works pre-code: pipe a written plan to `codex exec -` with a critique prompt ("review this plan — focus on bloat and YAGNI"). Useful before large implementations; findings adjust the plan, not the code.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Getting Unstuck: Repair Ladders for Common Git Failures
|
|
2
|
+
|
|
3
|
+
Each section is a failure you'll actually hit, with the sequence that resolves it.
|
|
4
|
+
|
|
5
|
+
## Rejected push (remote has new commits)
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
! [rejected] <branch> -> <branch> (fetch first)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The ladder — each step unblocks the next:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git stash push -u -m "wip before rebase" # only if the tree is dirty
|
|
15
|
+
git pull --rebase origin <branch>
|
|
16
|
+
git push
|
|
17
|
+
git stash pop
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- `git pull --rebase` refuses to run with unstaged changes (`cannot pull with rebase: You have unstaged changes`) — hence stash first, always with `-u` (untracked files) and a descriptive `-m`.
|
|
21
|
+
- If the dirty files are unrelated WIP, scope the stash: `git stash push -u -m "wip" -- <paths>` so the rest of the tree stays put.
|
|
22
|
+
- `fatal: Need to specify how to reconcile divergent branches` → same fix: `git pull --rebase origin <branch>`.
|
|
23
|
+
|
|
24
|
+
## Checkout/merge blocked by local changes
|
|
25
|
+
|
|
26
|
+
`error: Your local changes to the following files would be overwritten by checkout` — same stash-first pattern: stash, switch/merge, pop.
|
|
27
|
+
|
|
28
|
+
## Rebasing a stacked branch after its base was squash-merged
|
|
29
|
+
|
|
30
|
+
After the parent PR squash-merges, your stacked branch "contains" commits main already has in squashed form. A plain rebase replays them all and conflicts everywhere. Instead, replay only your own commits:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
git log --oneline main..HEAD # identify your commits
|
|
34
|
+
git merge-base HEAD origin/main # sanity-check the old fork point
|
|
35
|
+
git rebase --onto origin/main <old-base-sha> # replay only commits after <old-base-sha>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
During the rebase:
|
|
39
|
+
- **`--ours`/`--theirs` are inverted during rebase**: `--ours` is the *new base* (main), `--theirs` is *your branch's* change. `git checkout --ours <file> && git add <file>` keeps main's version.
|
|
40
|
+
- Commits that were already squash-merged become empty → `git rebase --skip` (or `git cherry-pick --skip` in cherry-pick flows; `--allow-empty` if you want the empty commit).
|
|
41
|
+
- "dropping <sha> ... patch contents already upstream" is rebase doing its job — verify afterward with `git log origin/main..HEAD` rather than assuming commits vanished.
|
|
42
|
+
- If you rebased a detached `HEAD`, reattach the branch: `git checkout -B <branch>`.
|
|
43
|
+
|
|
44
|
+
Then force-push: `git push --force-with-lease origin <branch>`, and confirm the PR recovered with `gh pr view <n> --json mergeable,mergeStateStatus`.
|
|
45
|
+
|
|
46
|
+
If a rebase goes sideways: `git rebase --abort`, re-inspect with `git log --oneline origin/main..HEAD`, try again with a better plan.
|
|
47
|
+
|
|
48
|
+
## Lockfile / generated-file conflicts during rebase
|
|
49
|
+
|
|
50
|
+
Don't hand-merge lockfiles. Take one side wholesale and regenerate:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
git checkout --ours yarn.lock && git add yarn.lock
|
|
54
|
+
git rebase --continue
|
|
55
|
+
yarn install # regenerate to match the merged manifest; commit if it changed
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## `--force-with-lease` rejected as stale
|
|
59
|
+
|
|
60
|
+
A bare `--force-with-lease` compares against your remote-tracking ref. If the branch was fetched into a local ref or `FETCH_HEAD` only (common in CI/sandbox checkouts), every lease push fails. Fix: make sure `refs/remotes/origin/<branch>` exists —
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
git fetch origin '+refs/heads/<branch>:refs/remotes/origin/<branch>'
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
— or pass an explicit lease: `--force-with-lease=<branch>:<expected-sha>`.
|
|
67
|
+
|
|
68
|
+
## Shallow clones silently imply single-branch
|
|
69
|
+
|
|
70
|
+
`git clone --depth=N` narrows the fetch refspec to the default branch, so `git fetch origin <other-branch> && git checkout <other-branch>` fails even though the branch exists. Fix with an explicit refspec:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git fetch origin '+refs/heads/<branch>:refs/remotes/origin/<branch>'
|
|
74
|
+
git checkout -B <branch> origin/<branch>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## `fatal: couldn't find remote ref <branch>`
|
|
78
|
+
|
|
79
|
+
You guessed the branch name. `git branch -a` / `git fetch origin` first — or skip the guessing entirely with `gh pr checkout <n>`, which fetches the PR head regardless of branch naming.
|
|
80
|
+
|
|
81
|
+
## `gh pr create` → "Head sha can't be blank / No commits between X and Y"
|
|
82
|
+
|
|
83
|
+
The branch has no commits ahead of its base (or wasn't pushed). Commit and/or `git push -u` first.
|
|
84
|
+
|
|
85
|
+
## Migrations vs a moving main
|
|
86
|
+
|
|
87
|
+
When main gained DB migrations while your PR was open: migrate down locally, rebase onto main, **rename your migration files so their timestamps sort after main's**, re-run migrations. Migration order is part of the merge.
|
|
88
|
+
|
|
89
|
+
## Worktrees: fix CI without disturbing WIP
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
git worktree add ../<repo>-hotfix <branch> # existing branch
|
|
93
|
+
git worktree add -b <new-branch> ../<repo>-hotfix origin/main
|
|
94
|
+
# ... fix, commit, push from the worktree ...
|
|
95
|
+
git worktree remove --force ../<repo>-hotfix && git worktree prune
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Caveat: hooks that run package scripts may fail inside a worktree if they resolve modules from the main checkout — run installs in the worktree first.
|
|
99
|
+
|
|
100
|
+
## `git diff --cached` can't take a range
|
|
101
|
+
|
|
102
|
+
`git diff --stat --cached main..` → usage error (exit 129). The cached diff is against a single commit: `git diff --stat --cached main`.
|
|
103
|
+
|
|
104
|
+
## Long-diverged automation branches
|
|
105
|
+
|
|
106
|
+
A bot-maintained branch diverged 3-vs-105 commits is not worth merging — `git reset --hard origin/<branch>` (destructive: requires explicit user authorization) and re-apply the local delta on top.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# gh api Recipes
|
|
2
|
+
|
|
3
|
+
Read-only `gh api` patterns for inspecting repos and PRs without checking anything out.
|
|
4
|
+
|
|
5
|
+
## PR feedback lives in TWO places — fetch both
|
|
6
|
+
|
|
7
|
+
Inline review comments and conversation-tab comments are different endpoints. When addressing PR feedback, always check both:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Inline review comments (attached to lines of the diff)
|
|
11
|
+
gh api repos/<owner>/<repo>/pulls/<n>/comments \
|
|
12
|
+
--jq '[.[] | {id, path, line, body, user: .user.login}]'
|
|
13
|
+
|
|
14
|
+
# Conversation-tab comments
|
|
15
|
+
gh api repos/<owner>/<repo>/issues/<n>/comments \
|
|
16
|
+
--jq '[.[] | {id, body, user: .user.login, created_at}]'
|
|
17
|
+
|
|
18
|
+
# Review states/bodies (approve / request-changes verdicts)
|
|
19
|
+
gh api repos/<owner>/<repo>/pulls/<n>/reviews --jq '[.[] | {state, body, user: .user.login}]'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Reply at the conversation level with `gh pr comment <n> --body "..."` (there's no clean CLI path for threaded inline replies).
|
|
23
|
+
|
|
24
|
+
Repo-wide recent PR comments, newest first:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
gh api 'repos/<owner>/<repo>/issues/comments?sort=created&direction=desc&per_page=30' --paginate \
|
|
28
|
+
--jq '.[] | select(.pull_request_url != null) | {pr: .pull_request_url, body, created_at}'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Reading files and history without a checkout
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Read one file off any branch (URL-encode brackets in paths: %5Bid%5D)
|
|
35
|
+
gh api 'repos/<owner>/<repo>/contents/<path>?ref=<branch>' --jq '.content' | base64 -d
|
|
36
|
+
|
|
37
|
+
# Full file tree
|
|
38
|
+
gh api 'repos/<owner>/<repo>/git/trees/<branch>?recursive=1' --jq '.tree[].path'
|
|
39
|
+
|
|
40
|
+
# Per-file commit history
|
|
41
|
+
gh api 'repos/<owner>/<repo>/commits?path=<file>&per_page=5' \
|
|
42
|
+
--jq '.[] | {sha: .sha[0:7], msg: .commit.message, date: .commit.author.date}'
|
|
43
|
+
|
|
44
|
+
# Which files a PR touches (with per-file status)
|
|
45
|
+
gh api repos/<owner>/<repo>/pulls/<n>/files --jq '.[] | {filename, status, additions, deletions}'
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
(`gh pr diff <n>` and `gh pr diff <n> --name-only` cover the common cases without the api call.)
|
|
49
|
+
|
|
50
|
+
## Repo settings audit
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Merge policy
|
|
54
|
+
gh api repos/<owner>/<repo> --jq '{allow_merge_commit, allow_squash_merge, allow_rebase_merge, squash_merge_commit_title, squash_merge_commit_message}'
|
|
55
|
+
|
|
56
|
+
# Branch protection
|
|
57
|
+
gh api repos/<owner>/<repo>/branches/<branch>/protection
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Deploy keys (server pulls a private repo)
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# generate the key on the server, then:
|
|
64
|
+
gh repo deploy-key add - --repo <owner>/<repo> --title "<host>" <<< "$PUBKEY"
|
|
65
|
+
gh repo deploy-key list --repo <owner>/<repo>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Token and version gotchas
|
|
69
|
+
|
|
70
|
+
- **Fine-grained PATs can't access some endpoints at all** (e.g. `/notifications`): 403 "Resource not accessible by personal access token". The tell: the response **lacks the `x-accepted-github-permissions` header**, meaning no grantable permission fixes it — you need a classic PAT with the right scope for that one call.
|
|
71
|
+
- For scripted clones with a token, the remote form is `https://x-access-token:<TOKEN>@github.com/<owner>/<repo>.git`.
|
|
72
|
+
- **Pushing workflow files** with an OAuth-scoped token fails with "refusing to allow an OAuth App to update workflow". Fix: `gh auth refresh -h github.com -s repo,workflow`.
|
|
73
|
+
- **`--json` field sets vary by gh version** (`Unknown JSON field: "isLatest"`). The error prints the supported field list — re-run with fields from that list rather than guessing.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Releases & Publishing
|
|
2
|
+
|
|
3
|
+
## Tags and GitHub releases
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
# Latest existing version tag, without a local checkout of all tags
|
|
7
|
+
git ls-remote --tags --refs --sort=-v:refname origin 'v*' | head -1
|
|
8
|
+
|
|
9
|
+
gh release create v<X.Y.Z> --target main --title "v<X.Y.Z>" --notes "$(cat <<'EOF'
|
|
10
|
+
## Changes
|
|
11
|
+
- ...
|
|
12
|
+
EOF
|
|
13
|
+
)"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## npm Trusted Publishing (OIDC, no NPM_TOKEN)
|
|
17
|
+
|
|
18
|
+
Publish from GitHub Actions with provenance and zero long-lived secrets:
|
|
19
|
+
|
|
20
|
+
1. On npmjs.com, configure the package's **Trusted Publisher**: the repo and the **exact workflow filename** (e.g. `release.yml`). The match is on the workflow file path — renaming the workflow breaks publishing.
|
|
21
|
+
2. The workflow job needs `permissions: id-token: write` and a registry-aware setup:
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
permissions:
|
|
25
|
+
contents: write
|
|
26
|
+
id-token: write
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v5
|
|
29
|
+
- uses: actions/setup-node@v5
|
|
30
|
+
with:
|
|
31
|
+
node-version: 24
|
|
32
|
+
registry-url: https://registry.npmjs.org
|
|
33
|
+
- run: npm publish # no NODE_AUTH_TOKEN needed
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
3. The published version must be new — Trusted Publishing doesn't bypass the "version already exists" check.
|
|
37
|
+
4. Verify provenance landed:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm view <pkg> dist.attestations.provenance.predicateType
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## release-please (tag/changelog automation)
|
|
44
|
+
|
|
45
|
+
Conventional commits (`feat:`, `fix:`, `chore(release):` etc.) drive everything: release-please opens/updates a release PR collecting changes; merging that PR creates the tag + GitHub release, which triggers the publish workflow. With this in place, never hand-create tags — just merge the release PR.
|
|
46
|
+
|
|
47
|
+
## Bootstrapping a repo
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
gh repo create <owner>/<name> --private --source=. --remote=origin --push [--description "..."]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
If the push step fails inside this compound command (often a pre-push hook), the repo was still created — push separately and read the real error (see SKILL.md on hook noise).
|