diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml new file mode 100644 index 0000000..bf779fc --- /dev/null +++ b/.github/workflows/auto-release.yml @@ -0,0 +1,80 @@ +# Boatstack-owned control plane. +name: Publish verified Boatstack release + +on: + workflow_run: + workflows: ["Verify Boatstack distribution"] + types: [completed] + +permissions: + contents: read + +concurrency: + group: auto-release-boatstack + cancel-in-progress: false + +jobs: + release: + if: >- + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' && + github.event.workflow_run.head_branch == 'main' + runs-on: ubuntu-latest + steps: + - name: Create repository automation token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.BOATSTACK_APP_CLIENT_ID }} + private-key: ${{ secrets.BOATSTACK_APP_PRIVATE_KEY }} + owner: operatorstack + repositories: boatstack + permission-contents: write + - uses: actions/checkout@v4 + with: + ref: main + fetch-depth: 0 + token: ${{ steps.app-token.outputs.token }} + - uses: actions/setup-go@v5 + with: + go-version-file: boatstack/go.mod + cache-dependency-path: boatstack/go.mod + - name: Detect release-bearing projection + id: classify + shell: bash + run: | + if [[ ! -f boatstack/release.go ]]; then + echo "The release classifier is not projected yet; no tag will be created." + echo "release_required=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + latest_tag="$(git describe --tags --abbrev=0 --match 'v[0-9]*' 2>/dev/null || true)" + if [[ -z "$latest_tag" ]]; then + echo "BLOCKED: automatic patch releases require an existing stable tag." >&2 + exit 1 + fi + classification="$(cd boatstack && go run ./cmd/boatstack-helper \ + release-classify --repo .. --base "$latest_tag" --head HEAD)" + printf '%s\n' "$classification" >> "$GITHUB_OUTPUT" + echo "latest_tag=$latest_tag" >> "$GITHUB_OUTPUT" + - name: Create next verified patch tag + if: steps.classify.outputs.release_required == 'true' + env: + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + LATEST_TAG: ${{ steps.classify.outputs.latest_tag }} + shell: bash + run: | + next_tag="$(cd boatstack && go run ./cmd/boatstack-helper \ + next-patch --version "$LATEST_TAG")" + if git rev-parse --verify --quiet "refs/tags/$next_tag"; then + echo "BLOCKED: tag already exists: $next_tag" >&2 + exit 1 + fi + git config user.name "${APP_SLUG}[bot]" + git config user.email "${APP_SLUG}[bot]@users.noreply.github.com" + git tag -a "$next_tag" -m "Boatstack $next_tag" + git push origin "$next_tag" + echo "Published verified release tag $next_tag." + - name: Report documentation-only sync + if: steps.classify.outputs.release_required != 'true' + run: echo "Boatstack content is current; this merge does not require new binaries." diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d3fd70..5258c70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,3 +67,43 @@ jobs: $errors | ForEach-Object { Write-Error $_ } exit 1 } + + auto-merge-sync: + if: >- + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository && + startsWith(github.head_ref, 'sync/intelligence-flow-') + needs: test + runs-on: ubuntu-latest + steps: + - name: Create repository automation token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.BOATSTACK_APP_CLIENT_ID }} + private-key: ${{ secrets.BOATSTACK_APP_PRIVATE_KEY }} + owner: operatorstack + repositories: boatstack + permission-contents: write + permission-pull-requests: write + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Verify generated projection provenance + env: + APP_SLUG: ${{ steps.app-token.outputs.app-slug }} + HEAD_BRANCH: ${{ github.head_ref }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + shell: bash + run: | + source_repo="$(jq -r '.source.repo' UPSTREAM.json)" + source_commit="$(jq -r '.source.commit' UPSTREAM.json)" + short="${source_commit:0:12}" + [[ "$PR_AUTHOR" == "${APP_SLUG}[bot]" ]] + [[ "$source_repo" == "operatorstack/intelligence-flow" ]] + [[ "$HEAD_BRANCH" == "sync/intelligence-flow-$short" ]] + - name: Merge verified generated PR + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + PR_URL: ${{ github.event.pull_request.html_url }} + run: gh pr merge "$PR_URL" --squash diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml index c6960a8..7f251ea 100644 --- a/.github/workflows/sync-upstream.yml +++ b/.github/workflows/sync-upstream.yml @@ -5,6 +5,11 @@ on: schedule: - cron: "17 */6 * * *" workflow_dispatch: + inputs: + source_commit: + description: Exact Intelligence Flow commit to project (defaults to main) + required: false + type: string permissions: contents: write @@ -18,29 +23,53 @@ jobs: sync: runs-on: ubuntu-latest steps: + - name: Create repository automation token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.BOATSTACK_APP_CLIENT_ID }} + private-key: ${{ secrets.BOATSTACK_APP_PRIVATE_KEY }} + owner: operatorstack + repositories: | + intelligence-flow + boatstack + permission-contents: write + permission-pull-requests: write - name: Check out Boatstack uses: actions/checkout@v4 with: path: boatstack-repo + token: ${{ steps.app-token.outputs.token }} - name: Check out Intelligence Flow uses: actions/checkout@v4 with: repository: operatorstack/intelligence-flow - ref: main + ref: ${{ inputs.source_commit || 'main' }} + fetch-depth: 0 path: intelligence-flow + token: ${{ steps.app-token.outputs.token }} - name: Generate projection id: generate shell: bash run: | source_commit="$(git -C intelligence-flow log -1 --format=%H -- examples/12-product-engineering-loop)" + current_commit="$(jq -r '.source.commit // empty' boatstack-repo/UPSTREAM.json)" + if [[ -n "$current_commit" ]] && + ! git -C intelligence-flow merge-base --is-ancestor "$current_commit" "$source_commit"; then + echo "Ignoring stale projection request $source_commit; Boatstack already records $current_commit." + echo "stale=true" >> "$GITHUB_OUTPUT" + exit 0 + fi python3 intelligence-flow/examples/12-product-engineering-loop/scripts/build_boatstack.py \ --repo boatstack-repo \ --source-commit "$source_commit" \ --write echo "source_commit=$source_commit" >> "$GITHUB_OUTPUT" + echo "stale=false" >> "$GITHUB_OUTPUT" - name: Open generated pull request + if: steps.generate.outputs.stale != 'true' env: - GH_TOKEN: ${{ github.token }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} SOURCE_COMMIT: ${{ steps.generate.outputs.source_commit }} shell: bash run: | @@ -92,13 +121,14 @@ jobs: echo "Upstream PR already open: $existing" exit 0 fi - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config user.name "${{ steps.app-token.outputs.app-slug }}[bot]" + git config user.email "${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com" git switch -c "$branch" git commit -m "Sync Boatstack from Intelligence Flow $short" git push --set-upstream origin "$branch" - gh pr create \ + pr_url="$(gh pr create \ --base main \ --head "$branch" \ --title "Sync Boatstack from Intelligence Flow $short" \ - --body-file "$body_file" + --body-file "$body_file")" + echo "Opened generated PR: $pr_url"