diff --git a/.github/scripts/sync-template.sh b/.github/scripts/sync-template.sh index 308acbeb..fe6950b4 100755 --- a/.github/scripts/sync-template.sh +++ b/.github/scripts/sync-template.sh @@ -284,6 +284,7 @@ files=("${filtered_files[@]}") standard_workflow() { case "$1" in ci.yml|codeql.yml|draft-pr.yml|release-pr.yml|release.yml|security.yml|test.yml) return 0 ;; + reusable-draft-pr.yml|reusable-release-pr.yml) return 0 ;; *) return 1 ;; esac } diff --git a/.github/workflows/reusable-draft-pr.yml b/.github/workflows/reusable-draft-pr.yml new file mode 100644 index 00000000..8c8c7fef --- /dev/null +++ b/.github/workflows/reusable-draft-pr.yml @@ -0,0 +1,51 @@ +name: Code Foundry Draft PR + +on: + workflow_call: + +permissions: + contents: read + pull-requests: write + +env: + GH_TOKEN: ${{ github.token }} + +jobs: + create-draft-pr: + name: Create + runs-on: ubuntu-slim + timeout-minutes: 10 + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + steps: + - name: Check + id: check-pr + run: | + BRANCH="${{ github.ref_name }}" + EXISTING=$(gh pr list --limit 1 --base staging --head "$BRANCH" --state open --json number,title 2>/dev/null | jq 'length') + echo "existing=$EXISTING" >> "$GITHUB_OUTPUT" + + - name: Create + if: steps.check-pr.outputs.existing == '0' + run: | + BRANCH="${{ github.ref_name }}" + gh pr create \ + --base staging \ + --head "$BRANCH" \ + --draft \ + --title "[WIP] $BRANCH" \ + --body "Draft PR created automatically by CI. + + This PR is a draft and should not be merged until CI passes and a reviewer (or the Daily Review agent) marks it ready. + + **CI Status:** Pending — this workflow triggered on push to \`$BRANCH\`." + + - name: Mark ready + if: steps.check-pr.outputs.existing == '0' + run: | + BRANCH="${{ github.ref_name }}" + PR_NUMBER=$(gh pr list --limit 1 --base staging --head "$BRANCH" --state open --json number 2>/dev/null | jq '.[0].number') + if [ -n "$PR_NUMBER" ]; then + gh pr ready "$PR_NUMBER" + fi diff --git a/.github/workflows/reusable-release-pr.yml b/.github/workflows/reusable-release-pr.yml new file mode 100644 index 00000000..107c5677 --- /dev/null +++ b/.github/workflows/reusable-release-pr.yml @@ -0,0 +1,86 @@ +name: Code Foundry Release PR + +on: + workflow_call: + +permissions: + contents: read + pull-requests: write + +env: + GH_TOKEN: ${{ github.token }} + +jobs: + create-release-pr: + name: Create + runs-on: ubuntu-slim + timeout-minutes: 10 + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + steps: + - name: Check + id: check-pr + run: | + set -euo pipefail + EXISTING=$(gh pr list \ + --repo "$GITHUB_REPOSITORY" \ + --limit 1 \ + --base main \ + --head staging \ + --state open \ + --json number | jq 'length') + echo "existing=$EXISTING" >> "$GITHUB_OUTPUT" + + - name: Create + if: steps.check-pr.outputs.existing == '0' + run: | + DATE=$(date +%Y-%m-%d) + BODY_FILE="$RUNNER_TEMP/release-pr.md" + COMPARE_URL="repos/${GITHUB_REPOSITORY}/compare/main...staging" + COMPARE_FILE="$RUNNER_TEMP/release-compare.json" + gh api "$COMPARE_URL?per_page=100" > "$COMPARE_FILE" + TOTAL_COMMITS=$(jq -r '.total_commits // 0' "$COMPARE_FILE") + if [ "$TOTAL_COMMITS" -gt 100 ]; then + LAST_PAGE=$(( (TOTAL_COMMITS + 99) / 100 )) + gh api "$COMPARE_URL?per_page=100&page=$LAST_PAGE" > "$COMPARE_FILE" + fi + COMMITS=$(jq -r '.commits[] | "- \(.commit.message | split("\\n")[0]) (\(.sha[0:7]))"' "$COMPARE_FILE") + if [ -z "$COMMITS" ]; then + COMMITS='- No commits found since main.' + fi + + if [ "$TOTAL_COMMITS" -eq 1 ]; then + CHANGE_LABEL=change + else + CHANGE_LABEL=changes + fi + + cat > "$BODY_FILE" < main ($DATE)" \ + --body-file "$BODY_FILE" diff --git a/README.md b/README.md index 713098b6..0b5e9a82 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,11 @@ main-branch wrapper while the shared runtime handles Release Please, GitHub release metadata, optional npm publication, provenance, and token/trusted publishing fallback. +The promotion runtimes for Draft PR and Release PR are maintained in the same +versioned repository. They are introduced before their consumer wrappers move +to a release tag, avoiding a bootstrap cycle when a new runtime version is +being published. + Initialization defaults to AGPL for new repositories. Synchronization defaults to `--license preserve`, so an existing repository's license is never replaced unless you explicitly select a license or provide `--license-file`.