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
56 changes: 44 additions & 12 deletions .github/workflows/promote.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ on:
permissions:
contents: read
checks: read
actions: read
pull-requests: write

jobs:
Expand Down Expand Up @@ -71,15 +72,25 @@ jobs:
exit 1
}

# The self-hosted runner has curl and jq but not the gh CLI
# (issue #460), so promotion queries the GitHub REST API directly.
api() {
curl -fsS \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"$@"
}

# A green source branch is a prerequisite, not a substitute for the
# target PR checks. Select the newest completed run for each check so
# an old green run cannot satisfy this gate. Documentation-only
# commits intentionally have no image build; a release tag builds
# the deployable artifact later.
for check in ci runner-policy; do
conclusion="$(gh api \
"repos/$GITHUB_REPOSITORY/commits/$source_sha/check-runs" \
--jq "[.check_runs[] | select(.name == \"$check\" and .status == \"completed\")] | sort_by(.completed_at) | last | .conclusion")"
conclusion="$(api \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/commits/$source_sha/check-runs?per_page=100" \
| jq -r "[.check_runs[] | select(.name == \"$check\" and .status == \"completed\")] | sort_by(.completed_at) | last | .conclusion")"
[ "$conclusion" = success ] || {
echo "::error::$SOURCE@$source_sha does not have a successful $check check (got ${conclusion:-missing})."
exit 1
Expand All @@ -88,13 +99,16 @@ jobs:

if [ "$SOURCE" = dev ]; then
receipt_name="vogt-dev-deployment-receipt-${source_sha}"
receipt_run="$(gh run list --repo "$GITHUB_REPOSITORY" --workflow deploy-dev.yml --json databaseId,headSha,status,conclusion --limit 50 \
--jq ".[] | select(.headSha == \"$source_sha\" and .status == \"completed\" and .conclusion == \"success\") | .databaseId" | head -n 1)"
receipt_run="$(api \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/workflows/deploy-dev.yml/runs?per_page=50" \
| jq -r ".workflow_runs[] | select(.head_sha == \"$source_sha\" and .status == \"completed\" and .conclusion == \"success\") | .id" | head -n 1)"
[ -n "$receipt_run" ] || {
echo "::error::dev@$source_sha has no successful verified dev deployment receipt ($receipt_name)."
exit 1
}
artifact_id="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$receipt_run/artifacts" --jq ".artifacts[] | select(.name == \"$receipt_name\") | .id" | head -n 1)"
artifact_id="$(api \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/actions/runs/$receipt_run/artifacts" \
| jq -r ".artifacts[] | select(.name == \"$receipt_name\") | .id" | head -n 1)"
[ -n "$artifact_id" ] || {
echo "::error::successful dev deployment has no matching receipt artifact."
exit 1
Expand Down Expand Up @@ -122,9 +136,22 @@ jobs:
run: |
set -euo pipefail
: "${GH_TOKEN:?Configure VOGT_PROMOTION_TOKEN with pull-request write access}"
existing="$(gh pr list --repo "$GITHUB_REPOSITORY" \
--head "$SOURCE" --base "$TARGET" --state open \
--json number,url --jq '.[0] // empty')"

# Talk to the REST API directly; the gh CLI is not on the runner
# (issue #460). This step authenticates as VOGT_PROMOTION_TOKEN so the
# created PR receives its normal checks.
api() {
curl -fsS \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"$@"
}

owner="${GITHUB_REPOSITORY%%/*}"
existing="$(api \
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/pulls?head=$owner:$SOURCE&base=$TARGET&state=open" \
| jq -r '.[0].html_url // empty')"
if [ -n "$existing" ]; then
echo "An open promotion PR already exists: $existing"
exit 0
Expand All @@ -148,6 +175,11 @@ jobs:
in this repository pushes a protected branch or deploys production as
a side effect of this PR.
EOF
gh pr create --repo "$GITHUB_REPOSITORY" --head "$SOURCE" --base "$TARGET" \
--title "Promote $SOURCE → $TARGET (${SOURCE_SHA:0:7})" \
--body-file "$body"
jq -n \
--arg title "Promote $SOURCE → $TARGET (${SOURCE_SHA:0:7})" \
--arg head "$SOURCE" \
--arg base "$TARGET" \
--rawfile body "$body" \
'{title: $title, head: $head, base: $base, body: $body}' \
| api -X POST "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/pulls" --data @- \
| jq -r '"Opened promotion PR: " + .html_url'
21 changes: 21 additions & 0 deletions docs/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,15 @@ overrides. The image records the resolved versions and refuses to start when a
persisted home volume would shadow an image-managed CLI (set
`VOGT_AGENT_SHADOW_POLICY=warn` only for a deliberate user-local override).

This build-time flag is the only way the CLIs enter a deployment, and it
defaults to `false`. No published image carries them: the release `vogt` core
image runs the register alone, and the signed release digests promoted to
production (§7) are CLI-free by design. So a deployment that runs agent
sessions is one whose engine was built with `VOGT_INSTALL_AI_CLIENTS=true` —
pinning a plain release image instead leaves the `Claude Code (protected)` and
`Codex (protected)` session templates registered but unable to start, because
the `claude` and `codex` binaries are simply not in the image.

Be aware before you run it: the engine image is a **development pod**, not a
hardened service image — it carries a writable home, `sudo`, optional agent
CLIs, and an entrypoint that supports integrations this repository's
Expand Down Expand Up @@ -367,6 +376,18 @@ a version tag creates signed, immutable artifacts; deployment selects the
digest a production instance runs. A successful build or published image does
not change production by itself.

The desired state a production instance runs — which digests, which overlays,
which host specifics — is owned by the operator's own deployment repository,
not this one (#204); this tree ships only the estate-neutral base and overlays,
never a turnkey production estate. Two consequences are worth stating plainly.
The signed release digests are CLI-free (§3.2), so a production engine that
runs agent sessions is one the operator built with
`VOGT_INSTALL_AI_CLIENTS=true` and published to its own registry — the release
`vogt`/`vogt-stack` digests are not that image. And the maintainer's own
production is one such private deployment, layering a private overlay on the
public base; it is not a supported drop-in scenario reproducible from this
repository alone.

### 7.1 Promote `dev` to production

Promotion is two explicit, fast-forward-only pull requests. First deploy the
Expand Down
Loading
Loading