Skip to content
Open
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
26 changes: 25 additions & 1 deletion .github/workflows/pulumi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ on:
description: 'Secrets provider for stack init (e.g. "gcpkms://projects/.../cryptoKeys/...")'
required: false
type: string
live-branch:
description: 'Branch tracking last successful `up`. Enforces monotonic ancestry and advances pointer on success.'
required: false
default: ''
type: string
secrets:
PULUMI_ACCESS_TOKEN:
description: 'Pulumi Cloud access token (if using Pulumi Cloud backend)'
Expand All @@ -76,12 +81,14 @@ jobs:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
contents: write
pull-requests: write
actions: read
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: ${{ inputs.live-branch && '0' || '1' }}

# GCP auth for Pulumi backend (gs://...) - only if vars are set
- name: Authenticate to Google Cloud
Expand Down Expand Up @@ -166,6 +173,19 @@ jobs:
echo "PULUMI_ACCESS_TOKEN=$PULUMI_ACCESS_TOKEN" >> $GITHUB_ENV
fi

- name: Verify ancestry from live-branch
if: inputs.cmd == 'up' && inputs.live-branch != ''
run: |
git fetch origin "${{ inputs.live-branch }}" || {
echo "::notice::live-branch '${{ inputs.live-branch }}' does not exist yet, skipping ancestry check"
exit 0
}
LIVE_SHA=$(git rev-parse "origin/${{ inputs.live-branch }}")
if ! git merge-base --is-ancestor "$LIVE_SHA" HEAD; then
echo "::error::Cannot up: HEAD is not a descendant of ${{ inputs.live-branch }} ($LIVE_SHA). Rebase first."
exit 1
fi

- name: Run Pulumi ${{ inputs.cmd }}
id: pulumi
working-directory: ${{ inputs.working-directory }}
Expand Down Expand Up @@ -280,6 +300,10 @@ jobs:

gh pr comment "$PR_NUMBER" --body-file comment-body.md

- name: Advance live-branch pointer
if: inputs.cmd == 'up' && inputs.live-branch != '' && steps.pulumi.outcome == 'success'
run: git push origin "HEAD:refs/heads/${{ inputs.live-branch }}"

- name: Export outputs
if: inputs.cmd == 'up'
working-directory: ${{ inputs.working-directory }}
Expand Down
100 changes: 100 additions & 0 deletions specs/done/aws-live-branch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# `live-branch` input: monotonic deployment pointer

## Problem

`pulumi up` can be dispatched from any branch (PR or main). Without coordination, two concurrent PRs could race — each `up`ing over the other, or a stale branch could revert a newer deployment.

## Design

The reusable workflow gets a new optional `live-branch` input. When set and `cmd == "up"`:

1. **Pre-step**: verify HEAD descends from the current `live-branch` pointer
2. **Run**: `pulumi up` as normal
3. **Post-step** (on success only): fast-forward `live-branch` to HEAD

### Ancestry check (pre-`up`)

```bash
git fetch origin "$LIVE_BRANCH" || {
echo "::notice::live-branch '$LIVE_BRANCH' does not exist yet, skipping ancestry check"
exit 0
}
LIVE_SHA=$(git rev-parse "origin/$LIVE_BRANCH")

if ! git merge-base --is-ancestor "$LIVE_SHA" HEAD; then
echo "::error::Cannot up: HEAD is not a descendant of $LIVE_BRANCH ($LIVE_SHA). Rebase onto $LIVE_BRANCH first."
exit 1
fi
```

### Update pointer (post-`up`)

```bash
git push origin "HEAD:refs/heads/$LIVE_BRANCH"
```

Fast-forward by construction (ancestry verified above). If two concurrent `up`s race, the second push fails (non-fast-forward) — desired behavior.

### `preview` and `refresh`

Unrestricted — no ancestry check, no pointer update. Any branch can `preview`.

## Changes to `pulumi.yml`

### New input

```yaml
inputs:
live-branch:
description: 'Branch to track last successful `up`. If set, enforces monotonic ancestry for `up` commands.'
required: false
type: string
default: ''
```

### New permission

```yaml
permissions:
contents: write # needed to push live-branch pointer (upgrade from read)
```

Note: this is only needed when `live-branch` is set. The workflow already has `contents: read`. The caller must also grant `contents: write`.

### New steps

Add two steps around the existing "Run Pulumi" step:

1. **Before** "Run Pulumi": ancestry check (conditional on `inputs.cmd == 'up' && inputs.live-branch != ''`)
2. **After** "Run Pulumi": update pointer (conditional on same + success)

## Caller usage (in `Open-Athena/ops`)

```yaml
jobs:
pulumi:
uses: Open-Athena/pulumi/.github/workflows/pulumi.yml@v1
with:
cmd: ${{ inputs.cmd }}
stack: ${{ inputs.stack }}
project: ${{ inputs.project }}
working-directory: aws/${{ inputs.project }}
deps-directory: aws
live-branch: aws-live
permissions:
contents: write
id-token: write
```

## Bootstrap

```bash
# In Open-Athena/ops: point aws-live at the last successfully up'd commit
# (currently HEAD of rw/pulumi-test-role, which was the last `up`)
git push o <last-up-sha>:refs/heads/aws-live
```

## Open questions

- Per-stack branches (`aws-live/oa-ci/dev`) vs single `aws-live`? Start with single, split later if needed.
- Add `concurrency` key to serialize `up` invocations? Push-time rejection is probably sufficient.