Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f9049e7
feat: add verified Rootform action
soulbah Aug 30, 2026
c2ece73
docs: refine public action surface (#2)
soulbah Aug 30, 2026
af45f02
fix(installer): reject draft releases
soulbah Aug 31, 2026
e8690b9
test(action): verify published release integration
soulbah Aug 31, 2026
5155d58
chore(deps): align bun tooling updates
soulbah Aug 31, 2026
d06c753
feat(action): report architecture changes (#8)
soulbah Aug 31, 2026
7b9d773
chore(toolchain): adopt bun 1.4 (#7)
soulbah Sep 1, 2026
1fb6fad
feat(action): prepare project dialects before analysis (#10)
soulbah Sep 1, 2026
f85519b
docs: prepare public repository controls (#11)
soulbah Sep 3, 2026
71eef75
ci: verify anonymous Rootform release install (#12)
soulbah Sep 3, 2026
2fafd9b
docs(readme): pin examples to rootform 0.1.1 (#13)
soulbah Sep 5, 2026
a8af71c
chore(deps-dev): bump @biomejs/biome from 2.5.11 to 2.5.12 in the fou…
dependabot[bot] Sep 7, 2026
001be5e
feat(action)!: prepare only lock-selected external packages (#16)
soulbah Sep 14, 2026
6a74ef7
docs(action): describe saved json as a rootform document (#17)
soulbah Sep 26, 2026
e0c20d1
chore(deps-dev): bump biome to 2.5.14 and semantic-release github to …
dependabot[bot] Sep 29, 2026
17cf0cb
feat!: redesign github actions around forms and native reports (#19)
soulbah Oct 2, 2026
8b026e9
fix(action): publish complete structured review reports (#20)
soulbah Oct 2, 2026
3033f51
docs(action): expose task guides and full user reference (#21)
soulbah Oct 2, 2026
5d4888c
docs(action): clarify concise input and output descriptions (#22)
soulbah Oct 2, 2026
170bde7
ci(release): release promoted main on dispatch and move major tags fo…
soulbah Oct 7, 2026
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
2 changes: 1 addition & 1 deletion .claude/rules/action-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ paths:
- Resolve a version to an exact release and verify its published checksum before the binary is executed or added to `PATH`.
- Inputs and outputs are a published contract. Renaming, removing, or redefining one requires an owner-accepted spec and a migration note.
- Request the least privilege that works. Document any permission beyond `contents: read` with the behavior that needs it.
- Never write a secret, token, absolute runner path, or raw Terraform material to a log, output, job summary, or artifact.
- Never write secrets, tokens, environment contents or raw Terraform inputs to logs, outputs, summaries, comments or artifacts. Step outputs contain file paths only; valid Forms and CLI reports are explicit derived evidence, allowed in enabled artifacts and human reports.
- `dist/` is generated. Rebuild it with its tool; never hand-edit it, and never commit a bundle that its source does not reproduce.
- Fail closed with a diagnostic naming the cause and the input that produced it. Silent fallback is forbidden.
2 changes: 2 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ updates:
time: "06:00"
timezone: "Europe/Paris"
open-pull-requests-limit: 5
ignore:
- dependency-name: "@types/bun"
groups:
foundation-minor-and-patch:
patterns:
Expand Down
38 changes: 30 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ on:
branches:
- dev
- main
push:
branches:
- dev
- main

permissions:
contents: read
Expand All @@ -30,10 +26,34 @@ jobs:
fetch-depth: 0
persist-credentials: false

- 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 quality passes, run the promote workflow for pull request #$PULL_REQUEST. Promotion publishes no release."
} >> "$GITHUB_STEP_SUMMARY"

- name: Install exact Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.14
bun-version: 1.4.0

- name: Install exact foundation dependencies
run: bun install --frozen-lockfile
Expand Down Expand Up @@ -74,14 +94,16 @@ jobs:
run: bun scripts/check-pr-metadata.ts

- name: Validate pull request commits
if: github.event_name == 'pull_request' && github.actor != 'dependabot[bot]'
# Commits are gated where they enter dev; a promotion carries those same commits.
if: github.base_ref == 'dev' && github.actor != 'dependabot[bot]'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: bun scripts/check-commit-range.ts

- name: Run full quality gate
env:
ROOTFORM_BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || '' }}
ROOTFORM_HEAD_BRANCH: ${{ github.event_name == 'pull_request' && github.head_ref || '' }}
# The product spec gate applies where changes enter dev; promotions carry gated changes.
ROOTFORM_BASE_SHA: ${{ github.base_ref == 'dev' && github.event.pull_request.base.sha || '' }}
ROOTFORM_HEAD_BRANCH: ${{ github.base_ref == 'dev' && github.head_ref || '' }}
run: bun run verify
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 quality
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: quality
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"
49 changes: 49 additions & 0 deletions .github/workflows/published-release-integration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: published release integration
run-name: Install published Rootform ${{ inputs.version }}

on:
workflow_dispatch:
inputs:
version:
description: Exact published Rootform version
required: true
type: string

permissions:
contents: read

concurrency:
group: published-release-integration-${{ inputs.version }}
cancel-in-progress: false

jobs:
setup:
name: install verified published release
runs-on: ubuntu-24.04
timeout-minutes: 10

steps:
- name: Check out Action
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install exact published Rootform release
id: rootform
uses: ./setup
with:
version: ${{ inputs.version }}
github-token: ""

- name: Verify installed release identity
shell: bash
env:
INSTALLED_SHA256: ${{ steps.rootform.outputs.sha256 }}
REQUESTED_VERSION: ${{ inputs.version }}
RESOLVED_VERSION: ${{ steps.rootform.outputs.version }}
run: |
set -euo pipefail
expected_version="${REQUESTED_VERSION#v}"
test "$RESOLVED_VERSION" = "$expected_version"
[[ "$INSTALLED_SHA256" =~ ^[0-9a-f]{64}$ ]]
test "$(rootform version)" = "rootform $expected_version"
90 changes: 85 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
name: release
run-name: Release the Action from main

on:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: read

concurrency:
group: release-${{ github.ref }}
group: release
cancel-in-progress: false

jobs:
Expand All @@ -19,9 +17,32 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
actions: read
checks: read
contents: write

steps:
- name: Require promoted main qualified against a stable Rootform release
id: qualification
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
fail() { echo "::error::$1"; exit 1; }
[[ "$GITHUB_REF" == refs/heads/main ]] || fail "Release the Action from main: gh workflow run release.yml --ref main."
main="$(gh api "repos/$GITHUB_REPOSITORY/branches/main" --jq .commit.sha)"
[[ "$main" == "$GITHUB_SHA" ]] || fail "main moved to $main after this run started. Dispatch again."
quality="$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/check-runs?check_name=quality&per_page=100" \
--jq '[.check_runs[] | select(.app.slug == "github-actions")] | sort_by(.started_at) | last | .conclusion // "missing"')"
[[ "$quality" == success ]] ||
fail "quality has not passed on $GITHUB_SHA (newest result: $quality). main changes only through a promotion pull request."
integration="$(gh api "repos/$GITHUB_REPOSITORY/actions/workflows/published-release-integration.yml/runs?head_sha=$GITHUB_SHA&status=success&per_page=100" \
--jq '[.workflow_runs[] | select(.display_title | test("^Install published Rootform v?[0-9]+[.][0-9]+[.][0-9]+$"))] | first | .html_url // empty')"
[[ -n "$integration" ]] ||
fail "No successful published release integration against a stable Rootform release on $GITHUB_SHA. Run gh workflow run published-release-integration.yml --ref main -f version=<stable Rootform version> first."
echo "integration=$integration" >> "$GITHUB_OUTPUT"
echo "main at \`$GITHUB_SHA\` passed quality and $integration." >> "$GITHUB_STEP_SUMMARY"

- name: Check out full history
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand All @@ -31,7 +52,7 @@ jobs:
- name: Install exact Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.14
bun-version: 1.4.0

- name: Install the Node runtime semantic-release requires
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
Expand All @@ -45,3 +66,62 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node ./node_modules/semantic-release/bin/semantic-release.js

- name: Confirm the release of this commit
id: release
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
fail() { echo "::error::$1"; exit 1; }
tag="$(git tag --points-at "$GITHUB_SHA" --list 'v*.*.*' | sort -V | tail -n 1)"
if [[ -z "$tag" ]]; then
echo "No feat, fix or breaking change since the previous release: nothing was published." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
if ! release="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$tag" 2>/dev/null)"; then
# semantic-release pushes the tag before it publishes the release; finish an interrupted run.
gh release create "$tag" --repo "$GITHUB_REPOSITORY" --verify-tag --generate-notes --title "$tag"
release="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$tag")"
fi
jq -e '.draft == false' <<<"$release" >/dev/null || fail "$tag is still a draft."
[[ "$(gh api "repos/$GITHUB_REPOSITORY/commits/$tag" --jq .sha)" == "$GITHUB_SHA" ]] || fail "$tag does not name $GITHUB_SHA."
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "Published $(jq -r .html_url <<<"$release") at \`$GITHUB_SHA\` (immutable: $(jq -r '.immutable // false' <<<"$release"))." >> "$GITHUB_STEP_SUMMARY"

- name: Point the major tag at this release
if: steps.release.outputs.tag != ''
env:
GH_TOKEN: ${{ github.token }}
INTEGRATION: ${{ steps.qualification.outputs.integration }}
RELEASE: ${{ steps.release.outputs.tag }}
run: |
set -euo pipefail
fail() { echo "::error::$1"; exit 1; }
if [[ ! "$RELEASE" =~ ^v([1-9][0-9]*)[.][0-9]+[.][0-9]+$ ]]; then
echo "$RELEASE is a 0.x or prerelease version: no major tag names it." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
major="v${BASH_REMATCH[1]}"
current=''
if ref="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$major" 2>/dev/null)"; then
current="$(jq -r .object.sha <<<"$ref")"
if [[ "$(jq -r .object.type <<<"$ref")" == tag ]]; then
current="$(gh api "repos/$GITHUB_REPOSITORY/git/tags/$current" --jq .object.sha)"
fi
fi
if [[ "$current" == "$GITHUB_SHA" ]]; then
echo "$major already names $RELEASE." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
if [[ -n "$current" ]]; then
status="$(gh api "repos/$GITHUB_REPOSITORY/compare/$current...$GITHUB_SHA" --jq .status)"
[[ "$status" == ahead ]] ||
fail "$major names $current, which $RELEASE does not descend from (comparison: $status). A major tag only moves forward."
gh api -X PATCH "repos/$GITHUB_REPOSITORY/git/refs/tags/$major" -f sha="$GITHUB_SHA" -F force=true >/dev/null
else
gh api -X POST "repos/$GITHUB_REPOSITORY/git/refs" -f ref="refs/tags/$major" -f sha="$GITHUB_SHA" >/dev/null
fi
[[ "$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$major" --jq .object.sha)" == "$GITHUB_SHA" ]] ||
fail "$major does not name $GITHUB_SHA after the update."
echo "$major now names $RELEASE at \`$GITHUB_SHA\` (previously \`${current:-absent}\`), qualified by $INTEGRATION." >> "$GITHUB_STEP_SUMMARY"
8 changes: 8 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,11 @@ title = "Rootform secret scanning"

[extend]
useDefault = true

[[allowlists]]
description = "Official Azure SDK CRC64 WebAssembly and local emulator constants"
regexTarget = "line"
regexes = [
'''JqIgUgAEHIksECaig''',
'''AccountName=devstoreaccount1;AccountKey=[A-Za-z0-9+/=]+;BlobEndpoint=http://127\.0\.0\.1''',
]
8 changes: 1 addition & 7 deletions .releaserc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@
"branches": ["main"],
"tagFormat": "v${version}",
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits",
"releaseRules": [{ "breaking": true, "release": "minor" }]
}
],
["@semantic-release/commit-analyzer", { "preset": "conventionalcommits" }],
[
"@semantic-release/release-notes-generator",
{
Expand Down
Loading
Loading