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
37 changes: 30 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,26 +1,49 @@
name: Verify
on:
pull_request:
branches: [dev]
push:
branches: [dev]
branches: [dev, main]
permissions:
contents: read
concurrency:
group: verify-${{ github.ref }}
cancel-in-progress: true
jobs:
packages:
verify:
name: verify
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
- name: Require promotion to fast-forward main from dev
if: github.base_ref == 'main'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PROMOTION_SOURCE: ${{ github.event.pull_request.head.repo.full_name }}:${{ github.head_ref }}
PULL_REQUEST: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
if [[ "$PROMOTION_SOURCE" != "$GITHUB_REPOSITORY:dev" ]]; then
echo "::error::main receives only promotions from $GITHUB_REPOSITORY:dev. Target dev instead."
exit 1
fi
if ! git merge-base --is-ancestor "$BASE_SHA" "$HEAD_SHA"; then
echo "::error::main is not an ancestor of dev. Promotion must be a fast-forward."
exit 1
fi
{
echo "## Promotion"
echo
echo "Fast-forwards main from \`$BASE_SHA\` to the exact dev commit \`$HEAD_SHA\` ($(git rev-list --count "$BASE_SHA..$HEAD_SHA") commits)."
echo "Once verify passes, run the promote workflow for pull request #$PULL_REQUEST. Promotion publishes nothing; a version tag on main does."
} >> "$GITHUB_STEP_SUMMARY"
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
package-manager-cache: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.4.0
- name: Prepare pinned Rust toolchain
Expand Down
108 changes: 108 additions & 0 deletions .github/workflows/promote.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
name: promote

on:
workflow_dispatch:
inputs:
pull_request:
description: Open pull request from dev into main whose exact head passed verify
required: true
type: number

permissions:
contents: read

concurrency:
group: promote-main
cancel-in-progress: false

jobs:
promote:
name: Fast-forward main to validated dev
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
actions: read
contents: write
pull-requests: read
steps:
- name: Verify exact validated promotion
id: promotion
env:
GH_TOKEN: ${{ github.token }}
PULL_REQUEST: ${{ inputs.pull_request }}
REQUIRED_JOB: verify
run: |
set -euo pipefail
fail() { echo "::error::$1"; exit 1; }
[[ "$GITHUB_REF" == refs/heads/dev ]] || fail "Run promote from dev: gh workflow run promote.yml --ref dev -f pull_request=$PULL_REQUEST."
pr="$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PULL_REQUEST")"
jq -e --arg repo "$GITHUB_REPOSITORY" \
'.state == "open" and .base.ref == "main" and .head.ref == "dev" and .head.repo.full_name == $repo' \
<<<"$pr" >/dev/null || fail "Pull request #$PULL_REQUEST is not an open promotion from dev into main."
head="$(jq -r .head.sha <<<"$pr")"
dev="$(gh api "repos/$GITHUB_REPOSITORY/branches/dev" --jq .commit.sha)"
main="$(gh api "repos/$GITHUB_REPOSITORY/branches/main" --jq .commit.sha)"
[[ "$head" == "$dev" ]] || fail "dev moved to $dev after pull request head $head. Wait for checks on the new head."
status="$(gh api "repos/$GITHUB_REPOSITORY/compare/$main...$head" --jq .status)"
[[ "$status" == ahead ]] || fail "main $main cannot fast-forward to $head (comparison: $status)."
# The newest run of this promotion on the exact head whose required job ran decides.
runs="$(gh api "repos/$GITHUB_REPOSITORY/actions/workflows/ci.yml/runs?event=pull_request&head_sha=$head&per_page=100" |
jq -r --argjson pr "$PULL_REQUEST" \
'[.workflow_runs[] | select(any(.pull_requests[]; .number == $pr and .base.ref == "main"))] | sort_by(-.run_number) | .[].id')"
verdict=none
url=none
for id in $runs; do
job="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$id/jobs?per_page=100" |
jq -c --arg name "$REQUIRED_JOB" '[.jobs[] | select(.name == $name and .conclusion != "skipped")] | last // empty')"
[[ -n "$job" ]] || continue
verdict="$(jq -r '.status + "/" + (.conclusion // "pending")' <<<"$job")"
url="$(jq -r .html_url <<<"$job")"
break
done
[[ "$verdict" == completed/success ]] ||
fail "$REQUIRED_JOB has not passed for pull request #$PULL_REQUEST on exact head $head (newest result: $verdict, $url)."
{
echo "head=$head"
echo "main=$main"
echo "run=$url"
} >> "$GITHUB_OUTPUT"

- name: Check out exact promotion history
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
ref: ${{ steps.promotion.outputs.head }}

- name: Fast-forward main to the validated commit
env:
HEAD_SHA: ${{ steps.promotion.outputs.head }}
# A plain push refuses anything but a fast-forward.
run: git push origin "$HEAD_SHA:refs/heads/main"

- name: Confirm promotion
env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ steps.promotion.outputs.head }}
PREVIOUS_MAIN: ${{ steps.promotion.outputs.main }}
PULL_REQUEST: ${{ inputs.pull_request }}
VALIDATION_RUN: ${{ steps.promotion.outputs.run }}
run: |
set -euo pipefail
main="$(gh api "repos/$GITHUB_REPOSITORY/branches/main" --jq .commit.sha)"
if [[ "$main" != "$HEAD_SHA" ]]; then
echo "::error::main is $main, expected $HEAD_SHA."
exit 1
fi
merged=false
for _ in 1 2 3 4 5 6; do
merged="$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PULL_REQUEST" --jq .merged)"
[[ "$merged" != true ]] || break
sleep 5
done
[[ "$merged" == true ]] || echo "::warning::GitHub has not yet marked pull request #$PULL_REQUEST as merged."
{
echo "## Promotion"
echo
echo "main moved from \`$PREVIOUS_MAIN\` to \`$HEAD_SHA\`, the exact head validated by $VALIDATION_RUN."
echo "Pull request #$PULL_REQUEST merged: $merged."
} >> "$GITHUB_STEP_SUMMARY"
18 changes: 18 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: Release
# A version tag packages one VSIX, attaches it to a GitHub release and
# publishes that same file to Open VSX and the Visual Studio Marketplace.
# Only a tag on promoted main whose commit passed verify publishes.
# docs/releasing.md covers the one-time store setup and the Zed registry.
on:
push:
Expand All @@ -13,9 +14,26 @@ concurrency:
jobs:
package:
runs-on: ubuntu-24.04
permissions:
checks: read
contents: read
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Require a tag on promoted main that passed verify
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
fail() { echo "::error::$1"; exit 1; }
commit="$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_REF_NAME" --jq .sha)"
status="$(gh api "repos/$GITHUB_REPOSITORY/compare/$commit...main" --jq .status)"
[[ "$status" == ahead || "$status" == identical ]] ||
fail "$GITHUB_REF_NAME names $commit, which is not on main. Promote dev to main, then tag the main commit."
verify="$(gh api "repos/$GITHUB_REPOSITORY/commits/$commit/check-runs?check_name=verify&per_page=100" \
--jq '[.check_runs[] | select(.app.slug == "github-actions")] | sort_by(.started_at) | last | .conclusion // "missing"')"
[[ "$verify" == success ]] ||
fail "verify has not passed on $commit (newest result: $verify). Tag the main commit validated by its promotion pull request."
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
Expand Down
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ Read `docs/editor-contract.md` and the affected editor package before editing.
download Rootform or trigger Terraform/provider/cloud execution.
- Work on a branch and open a PR into `dev`. Never push directly to `main` or
`dev`, force push, bypass checks or modify another task's branch/PR.
- `main` moves only by promotion: a PR from `dev` into `main` whose head
passed `verify`, fast-forwarded by `promote.yml`. Release tags name a
`main` commit; `docs/releasing.md` describes the release.

Local development evidence stays outside the public repository. Do not commit
private implementation, project paths, inputs, credentials or operator notes.
20 changes: 13 additions & 7 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,27 @@ accepts no pre-release suffix, and no store accepts the same version twice.

1. On a branch, set the new version in the three manifests, add an entry to
`vscode/CHANGELOG.md` and open a pull request into `dev`.
2. After it merges, qualify the merge commit with the Rootform release it
targets: `bun run verify`, the VS Code extension-host suite with
2. After it merges, promote `dev`: open a pull request from `dev` into
`main`; once `verify` passes on its head, run
`gh workflow run promote.yml --ref dev -f pull_request=<number>`, which
fast-forwards `main` to that exact commit.
3. Qualify the `main` commit with the Rootform release it targets:
`bun run verify`, the VS Code extension-host suite with
`ROOTFORM_BINARY`, and the Zed package installed as a dev extension.
3. Tag the merge commit and push the tag:
4. Tag the `main` commit and push the tag:

```sh
git tag -a v0.1.0 -m "Rootform editors 0.1.0" <merge-commit>
git tag -a v0.1.0 -m "Rootform editors 0.1.0" <main-commit>
git push origin v0.1.0
```

4. Update the Zed registry, as described below.
5. Check each store page, then install from each store and open a `.rf.hcl`
5. Update the Zed registry, as described below.
6. Check each store page, then install from each store and open a `.rf.hcl`
file with a released Rootform CLI.

The [release workflow](../.github/workflows/release.yml) checks that the tag
The [release workflow](../.github/workflows/release.yml) refuses a tag whose
commit is not on `main` or did not pass `verify` in its promotion pull
request. It checks that the tag
matches the manifests, runs `verify`, packages one VSIX and attaches it with
`SHA256SUMS` to the GitHub release. It publishes that same file to Open VSX
and the Visual Studio Marketplace from the `release` environment, which only
Expand Down
Loading