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
174 changes: 174 additions & 0 deletions .github/workflows/release-planmonster.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
name: Release (GitHub assets, PlanMonster)

# Fork-owned GitHub Release only. This workflow intentionally does not invoke
# the upstream release workflow, GoReleaser, npm, Homebrew, or the MCP Registry.
# Its olk-pm-v* namespace cannot match release.yml's v* or publish-npm.yml's
# npm-v* trigger.

on:
push:
tags:
- "olk-pm-v*"

permissions:
contents: read

jobs:
version:
name: validate fork tag
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 5
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Validate tag and resolve version
id: version
run: |
set -euo pipefail
if ! printf '%s' "$GITHUB_REF_NAME" | grep -qE '^olk-pm-v[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "tag must match olk-pm-v<upstream-major>.<minor>.<patch>.<fork-revision>" >&2
echo "example: olk-pm-v1.11.0.1" >&2
exit 1
fi
version="${GITHUB_REF_NAME#olk-pm-v}"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "Building PlanMonster GitHub assets for $version" >> "$GITHUB_STEP_SUMMARY"

build-linux-windows:
name: build linux + windows
needs: version
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 30
steps:
- name: Checkout
uses: useblacksmith/checkout@6fd481652155169ed4d2f25ebaf97464f685175f # v1.1

- name: Set up Go
uses: useblacksmith/setup-go@647ac649bd5b480f2a262e3e3e5f4d150ed452ad # v6.0.1
with:
go-version-file: go.mod

- name: Build and archive
env:
VERSION: ${{ needs.version.outputs.version }}
CGO_ENABLED: "0"
run: |
set -euo pipefail
mkdir -p dist stage
ldflags="-s -w \
-X github.com/rlrghb/olkcli/internal/cmd.Version=${VERSION} \
-X github.com/rlrghb/olkcli/internal/cmd.Commit=${GITHUB_SHA} \
-X github.com/rlrghb/olkcli/internal/cmd.Date=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
build() { # goos goarch executable
goos="$1"; goarch="$2"; executable="$3"
rm -rf stage/*
GOOS="$goos" GOARCH="$goarch" go build -ldflags "$ldflags" -o "stage/$executable" ./cmd/olk
if [ "$goos" = windows ]; then
(cd stage && zip -q "../dist/olk_${VERSION}_${goos}_${goarch}.zip" "$executable")
else
tar -C stage -czf "dist/olk_${VERSION}_${goos}_${goarch}.tar.gz" "$executable"
fi
}
build linux amd64 olk
build linux arm64 olk
build windows amd64 olk.exe
build windows arm64 olk.exe
rm -rf stage
ls -lh dist

- name: Upload archives
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-linux-windows
path: dist/*
retention-days: 1

build-darwin:
name: build darwin
needs: version
runs-on: macos-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod

- name: Build and archive
env:
VERSION: ${{ needs.version.outputs.version }}
CGO_ENABLED: "1"
run: |
set -euo pipefail
mkdir -p dist stage
ldflags="-s -w \
-X github.com/rlrghb/olkcli/internal/cmd.Version=${VERSION} \
-X github.com/rlrghb/olkcli/internal/cmd.Commit=${GITHUB_SHA} \
-X github.com/rlrghb/olkcli/internal/cmd.Date=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
for goarch in amd64 arm64; do
rm -rf stage/*
GOOS=darwin GOARCH="$goarch" go build -ldflags "$ldflags" -o stage/olk ./cmd/olk
tar -C stage -czf "dist/olk_${VERSION}_darwin_${goarch}.tar.gz" olk
done
rm -rf stage
ls -lh dist

- name: Upload archives
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-darwin
path: dist/*
retention-days: 1

release:
name: create GitHub Release
needs: [version, build-linux-windows, build-darwin]
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Download archives
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: release-*
path: dist
merge-multiple: true

- name: Verify assets and generate checksums
env:
VERSION: ${{ needs.version.outputs.version }}
run: |
set -euo pipefail
cd dist
for asset in \
"olk_${VERSION}_linux_amd64.tar.gz" \
"olk_${VERSION}_linux_arm64.tar.gz" \
"olk_${VERSION}_darwin_amd64.tar.gz" \
"olk_${VERSION}_darwin_arm64.tar.gz" \
"olk_${VERSION}_windows_amd64.zip" \
"olk_${VERSION}_windows_arm64.zip"; do
test -s "$asset" || { echo "missing release asset: $asset" >&2; exit 1; }
done
sha256sum olk_* > checksums.txt
sha256sum --check checksums.txt

mkdir smoke
tar -xzf "olk_${VERSION}_linux_amd64.tar.gz" -C smoke
test "$(./smoke/olk version --json | jq -r .version)" = "$VERSION"
rm -rf smoke

- name: Create fork-only GitHub Release
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ needs.version.outputs.version }}
run: |
set -euo pipefail
gh release create "$GITHUB_REF_NAME" dist/* \
--repo "$GITHUB_REPOSITORY" \
--verify-tag \
--title "PlanMonster olk ${VERSION}" \
--generate-notes
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
- **No publish secrets:** npm uses **Trusted Publishing (OIDC)** (`id-token: write`, npm ≥ 11.5.1, SLSA provenance); the registry uses GitHub OIDC. The npm package is `olkcli`; the binary is `olk`.
- The official MCP registry has no in-place edit — `server.json` description/version changes apply on the **next release** (versions are CI-stamped from the tag). `npm-publish`/`registry-publish` are gated on the `PUBLISH_NPM` repo variable.
- **Fork npm distribution (`@planmonster`) — `publish-npm.yml`, this fork only.** An `npm-vX.Y.Z-<suffix>` tag (or a `workflow_dispatch` with `dry_run`) builds all six binaries (linux/windows on Blacksmith with `CGO_ENABLED=0`; darwin on `macos-latest` with `CGO_ENABLED=1`) and publishes `@planmonster/olkcli` + six `@planmonster/olk-<os>-<arch>` packages to npmjs.org via Trusted Publishing. The tag prefix is `npm-v`, not `v`, so it does not also fire `release.yml`. Versions **must** carry a fork suffix (e.g. `1.10.0-pm.1`); the workflow rejects a bare `X.Y.Z` because upstream owns those numbers. Because the suffix makes every version a semver prerelease, the dist-tag must always be explicit — `build-npm.mjs` defaults it to `latest` and stamps `publishConfig.tag`. `scripts/build-npm.mjs` also takes `--scope`, `--registry`, `--repository`, `--access`, and `--skip-binary-check` (bootstrap only); re-scoping is idempotent. All seven names now exist on npmjs.org (bootstrapped at `1.10.0-pm.1` with `scripts/bootstrap-npm.sh`). See `docs/npm-publishing.md`.
- **Fork GitHub assets — `release-planmonster.yml`, GitHub Release only.** A tag matching `olk-pm-vX.Y.Z.N` builds six checksummed native archives and creates a GitHub Release. The namespace cannot match upstream `v*` or fork npm `npm-v*`; the workflow does not invoke GoReleaser, Homebrew, npm, or the MCP Registry. NanoClaw consumes `olk_X.Y.Z.N_linux_amd64.tar.gz`. Never move/reuse a tag; increment `N`. See `docs/github-releases.md`.
- `release.yml` is the **upstream** pipeline and cannot run on this fork: the unscoped npm names belong to upstream, `TAP_GITHUB_TOKEN` does not exist here, and `io.github.rlrghb/outlook` is not our MCP namespace. Leave it inert rather than repurposing it.
- **ClawHub (OpenClaw skill) — manual, separate from the tag pipeline.** olk is listed on ClawHub as a **skill** from `SKILL.md`. Publish with the `clawhub` CLI (publisher `rlrghb`; `clawhub whoami` / `clawhub login`) from a folder containing **only `SKILL.md`** — `mkdir -p /tmp/olk-skill && cp SKILL.md /tmp/olk-skill/`, then `clawhub skill publish /tmp/olk-skill --slug olk --name Outlook --version <X.Y.Z> --tags calendar,contacts,drive,latest,mail,microsoft,onedrive,outlook,tasks --changelog '…'`. The skill version is **independent of the binary** — align it to the release. Display name is `Outlook` (pass `--name`; `SKILL.md`'s `name: olk` is only the slug). The summary comes from `SKILL.md` `description:`; **category** ("DATA & APIS") is web-UI only. **No `--dry-run`** — verify the live entry with `clawhub inspect olk` and confirm before publishing.

Expand All @@ -66,7 +67,7 @@
- **Lazy client init**: `RunContext.GraphClient()` initializes on first call — auth commands don't need a Graph client.
- **Delegated mailbox routing**: read paths in `internal/graphapi/{mail,calendar,contacts}.go` take a `target string` first parameter and route through `c.targetUser(target)`. Empty target preserves `/me` behavior; a non-empty value hits `/users/{target}/…`. The CLI exposes this as the global `--mailbox` flag (env `OLK_MAILBOX`), validated once via `resolveMailboxTarget` in `internal/cmd/paging.go`. New read methods should follow the same shape; write paths intentionally stay on `/me` for now.
- **MCP server**: `olk mcp` (`internal/cmd/mcp*.go`) exposes a curated, read-first allowlist of tools over stdio — not the whole CLI. Tool calls reparse argv and run in-process with stdout captured. Read-only by default; `--allow-write <tool>` exposes a named curated safe-write tool (per-tool opt-in). No HTTP transport (deliberate scope choice). To expose a command, add it to `curatedTools` (read or non-destructive write only).
- **Token mode (credential injection)** — `internal/cmd/token.go`. When `--access-token` / `OLK_ACCESS_TOKEN` is set, olk becomes a pure consumer of an externally minted **delegated Graph access token**: it never touches the keyring, the account files, or the default-account config; it never refreshes; it never persists the token. The token's lifetime is the process's lifetime. This exists so a disposable environment (CI, container, agent sandbox) holds no durable refresh token — a container-local keyring would both leak a long-lived credential and race Microsoft's refresh-token rotation across concurrent containers.
- **Token mode (credential injection)** — `internal/cmd/token.go`. When environment-only `OLK_ACCESS_TOKEN` is set, olk becomes a pure consumer of an externally minted **delegated Graph access token**: it never touches the keyring, the account files, or the default-account config; it never refreshes; it never persists the token. There is deliberately no `--access-token` option, so the credential cannot enter process arguments or Kong help/schema output. The token's lifetime is the process's lifetime. This exists so a disposable environment (CI, container, agent sandbox) holds no durable refresh token — a container-local keyring would both leak a long-lived credential and race Microsoft's refresh-token rotation across concurrent containers.
- `newTokenMode()` **fails closed**: an unparseable or already-passed `OLK_ACCESS_TOKEN_EXPIRES_AT` is an error raised *before* any network call and before any credential store is touched. A nil `*tokenMode` means account mode (the normal keyring path) — preserve that distinction when adding flags.
- Expiry maps to the dedicated exit code **77** (`exitTokenExpired`) via the `errTokenExpired` sentinel, so an orchestrator can mint a fresh token instead of treating the run as a hard failure. Do not fold it into the generic exit 1.
- With no supplied expiry, the Azure SDK is told `nominalTokenLifetime` (30 min) because a zero expiry makes its credential pipeline refuse to send the request; Graph's `401` is then the authority.
Expand Down
Loading
Loading