Skip to content

Commit 50c42ca

Browse files
blazejpawlakclaude
andcommitted
feat(ci): GPG signing via org secrets + 1Password environment locally
- Rewrite scripts/gpg-unlock.sh to use `op environment read` instead of `op read`, matching the bootstrap script's resolve_op_passphrase pattern; default env ID is the Rodzinka account environment (weksim7ulja7rrhkaap3lxk2wa) - Remove 1password/load-secrets-action from release.yml; import GPG key directly from org-level GitHub secrets (GPG_PRIVATE_KEY, GPG_PASSPHRASE) - Drop id-token: write permission (no longer needed without load-secrets-action) - Document GPG signing setup in AGENTS.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 614f7d0 commit 50c42ca

File tree

3 files changed

+94
-0
lines changed

3 files changed

+94
-0
lines changed

.github/workflows/release.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@ jobs:
4545
- name: Build
4646
run: bun run build
4747

48+
- name: Import GPG key
49+
uses: crazy-max/ghaction-import-gpg@cb9bde2e2525e640591a934b1fd28eef1dcaf5e5 # v6
50+
with:
51+
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
52+
passphrase: ${{ secrets.GPG_PASSPHRASE }}
53+
git_user_signingkey: true
54+
git_commit_gpgsign: true
55+
git_tag_gpgsign: true
56+
57+
- name: Set GPG_TTY
58+
run: echo "GPG_TTY=$(tty)" >> $GITHUB_ENV
59+
4860
- name: Release
4961
run: bun run release
5062
env:

AGENTS.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,29 @@ bd memories <keyword> # Search saved memories
102102
- Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files
103103
- Do NOT use `bd edit` — it opens an interactive editor that blocks agents
104104

105+
## GPG Signing
106+
107+
Commits and tags are signed with GPG key `60BFBD78D728EEE4`.
108+
109+
**Local — unlock the GPG agent (run once per session):**
110+
111+
```bash
112+
./scripts/gpg-unlock.sh
113+
```
114+
115+
Fetches the passphrase via `op environment read opencode-env` (1Password Environments) and presets it into `gpg-agent` so subsequent `git commit`/`git tag` calls don't prompt. Env var overrides:
116+
117+
| Variable | Default | Purpose |
118+
|---|---|---|
119+
| `GPG_SIGN_KEY` | `60BFBD78D728EEE4` | Key ID to unlock |
120+
| `OPENCODE_MANIFEST_SIGN_1PASSWORD_ENV_ID` | `opencode-env` | 1Password environment name |
121+
| `OPENCODE_MANIFEST_SIGN_1PASSWORD_ACCOUNT` | _(CLI default)_ | 1Password account |
122+
| `OPENCODE_MANIFEST_SIGN_1PASSWORD_VAR` | `OPENCODE_MANIFEST_SIGN_PASSPHRASE` | Variable name in the env |
123+
124+
**CI — GitHub Actions:**
125+
126+
`release.yml` uses `crazy-max/ghaction-import-gpg` with `secrets.GPG_PRIVATE_KEY` and `secrets.GPG_PASSPHRASE` stored as org-level GitHub secrets. The CI key is a dedicated key separate from the personal signing key — rotate it independently without touching local config. No 1Password service account is needed in CI.
127+
105128
## Testing
106129

107130
Unit tests live in `test/`. Run with `bun test`.

scripts/gpg-unlock.sh

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
SIGN_KEY="${GPG_SIGN_KEY:-60BFBD78D728EEE4}"
5+
OP_ENV="${OPENCODE_MANIFEST_SIGN_1PASSWORD_ENV_ID:-weksim7ulja7rrhkaap3lxk2wa}"
6+
OP_ACCOUNT="${OPENCODE_MANIFEST_SIGN_1PASSWORD_ACCOUNT:-}"
7+
OP_VAR="${OPENCODE_MANIFEST_SIGN_1PASSWORD_VAR:-OPENCODE_MANIFEST_SIGN_PASSPHRASE}"
8+
9+
# Resolve passphrase via 1Password environment
10+
resolve_passphrase() {
11+
local -a op_args
12+
op_args=(environment read "$OP_ENV")
13+
[[ -n "$OP_ACCOUNT" ]] && op_args+=(--account "$OP_ACCOUNT")
14+
15+
local env_output line key value
16+
env_output="$(OP_DISABLE_PROMPTS=1 op "${op_args[@]}" 2>/dev/null)" || {
17+
echo "Failed to read 1Password environment '$OP_ENV'" >&2; exit 1
18+
}
19+
20+
while IFS= read -r line; do
21+
[[ -z "$line" ]] && continue
22+
key="${line%%=*}"
23+
value="${line#*=}"
24+
if [[ "$key" == "$OP_VAR" ]]; then
25+
printf '%s' "$value"
26+
return 0
27+
fi
28+
done <<< "$env_output"
29+
30+
echo "Variable '$OP_VAR' not found in environment '$OP_ENV'" >&2
31+
exit 1
32+
}
33+
34+
keygrip_for_sign_key() {
35+
local line keygrip
36+
while IFS= read -r line; do
37+
case "$line" in
38+
grp:*)
39+
keygrip="${line#grp:::::::::}"
40+
keygrip="${keygrip%%:*}"
41+
[[ -n "$keygrip" ]] && { printf '%s' "$keygrip"; return 0; }
42+
;;
43+
esac
44+
done < <(gpg --with-colons --with-keygrip --list-secret-keys "$SIGN_KEY" 2>/dev/null)
45+
return 1
46+
}
47+
48+
SIGN_PASSPHRASE="$(resolve_passphrase)"
49+
50+
libexecdir="$(gpgconf --list-dirs libexecdir 2>/dev/null)"
51+
preset_bin="${libexecdir%/}/gpg-preset-passphrase"
52+
[[ ! -x "$preset_bin" ]] && preset_bin="$(command -v gpg-preset-passphrase 2>/dev/null || true)"
53+
[[ -z "$preset_bin" || ! -x "$preset_bin" ]] && { echo "gpg-preset-passphrase not found" >&2; exit 1; }
54+
55+
keygrip="$(keygrip_for_sign_key)"
56+
[[ -z "$keygrip" ]] && { echo "Could not get keygrip for $SIGN_KEY" >&2; exit 1; }
57+
58+
"$preset_bin" -c -P "$SIGN_PASSPHRASE" "$keygrip"
59+
echo "GPG agent unlocked for key $SIGN_KEY"

0 commit comments

Comments
 (0)