code-foundry 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.editorconfig +18 -0
  2. package/.env.example +3 -0
  3. package/.gitattributes +9 -0
  4. package/.githooks/pre-commit +67 -0
  5. package/.github/CODEOWNERS +4 -0
  6. package/.github/CODE_OF_CONDUCT.md +41 -0
  7. package/.github/CONTRIBUTING.md +198 -0
  8. package/.github/ISSUE_TEMPLATE/bug_report.yml +56 -0
  9. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  10. package/.github/ISSUE_TEMPLATE/feature_request.yml +38 -0
  11. package/.github/PULL_REQUEST_TEMPLATE.md +29 -0
  12. package/.github/SECURITY.md +23 -0
  13. package/.github/actions/cache/action.yml +43 -0
  14. package/.github/actions/codeql/action.yml +29 -0
  15. package/.github/actions/setup/action.yml +776 -0
  16. package/.github/dependabot.yml +42 -0
  17. package/.github/scripts/bootstrap.sh +20 -0
  18. package/.github/scripts/changed-files.sh +96 -0
  19. package/.github/scripts/ci.sh +636 -0
  20. package/.github/scripts/codeql-languages.sh +86 -0
  21. package/.github/scripts/doctor.sh +103 -0
  22. package/.github/scripts/format-fast-path.sh +80 -0
  23. package/.github/scripts/init-repo.sh +134 -0
  24. package/.github/scripts/security.sh +209 -0
  25. package/.github/scripts/sitecustomize.py +17 -0
  26. package/.github/scripts/sync-codeowners.sh +76 -0
  27. package/.github/scripts/sync-protection.sh +137 -0
  28. package/.github/scripts/sync-template.sh +432 -0
  29. package/.github/scripts/turbo-cache-probe.sh +102 -0
  30. package/.github/template.yml +7 -0
  31. package/.github/template.yml.example +8 -0
  32. package/.github/workflows/ci.yml +166 -0
  33. package/.github/workflows/codeql.yml +160 -0
  34. package/.github/workflows/draft-pr.yml +59 -0
  35. package/.github/workflows/release-pr.yml +66 -0
  36. package/.github/workflows/release.yml +103 -0
  37. package/.github/workflows/security.yml +228 -0
  38. package/.github/workflows/test.yml +175 -0
  39. package/.gitignore +77 -0
  40. package/.mise.toml +6 -0
  41. package/.prettierrc +6 -0
  42. package/AGENTS.md +155 -0
  43. package/CHANGELOG.md +3 -0
  44. package/LICENSE +616 -0
  45. package/NOTICE +7 -0
  46. package/README.md +172 -0
  47. package/package.json +44 -0
  48. package/release-please-config.json +16 -0
  49. package/ruff.toml +6 -0
  50. package/src/cli.mjs +137 -0
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # A Turbo remote-cache hit can be resolved from package manifests alone. This
5
+ # probe lets CI skip the full dependency install when every executable task is
6
+ # already cached. It is deliberately conservative: only Bun projects with a
7
+ # pinned Turbo version are eligible, and synthetic dependency-only tasks are
8
+ # excluded from the hit decision.
9
+
10
+ task="${1:?task name required}"
11
+
12
+ set_output() {
13
+ printf '%s=%s\n' "$1" "$2" >> "${GITHUB_OUTPUT:-/dev/null}"
14
+ }
15
+
16
+ set_output hit false
17
+
18
+ [ -n "${TURBO_TOKEN:-}" ] || exit 0
19
+ [ -n "${TURBO_TEAM:-}" ] || exit 0
20
+ [ -f package.json ] || exit 0
21
+ [ -f bun.lock ] || [ -f bun.lockb ] || exit 0
22
+
23
+ turbo_version="$(node <<'NODE'
24
+ const fs = require('node:fs');
25
+ const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
26
+ const groups = [
27
+ packageJson.devDependencies,
28
+ packageJson.dependencies,
29
+ packageJson.optionalDependencies,
30
+ packageJson.pnpm?.overrides,
31
+ ];
32
+ for (const group of groups) {
33
+ const value = group?.turbo;
34
+ const match = typeof value === 'string' && value.match(/(\d+\.\d+\.\d+)/);
35
+ if (match) {
36
+ console.log(match[1]);
37
+ break;
38
+ }
39
+ }
40
+ NODE
41
+ )"
42
+ [ -n "$turbo_version" ] || exit 0
43
+
44
+ turbo_home="${RUNNER_TEMP:-/tmp}/repo-foundry-turbo"
45
+ probe_file="$(mktemp)"
46
+ install_file="$(mktemp)"
47
+ result_file="$(mktemp)"
48
+ cleanup() { rm -f "$probe_file" "$install_file" "$result_file"; }
49
+ trap cleanup EXIT
50
+
51
+ if ! npm install --prefix "$turbo_home" --no-save --ignore-scripts --no-audit --no-fund \
52
+ "turbo@${turbo_version}" >"$install_file" 2>&1; then
53
+ echo "Turbo Probe: CLI unavailable; using full install"
54
+ exit 0
55
+ fi
56
+
57
+ turbo_bin="$turbo_home/node_modules/.bin/turbo"
58
+ if [ ! -x "$turbo_bin" ]; then
59
+ echo "Turbo Probe: CLI unavailable; using full install"
60
+ exit 0
61
+ fi
62
+
63
+ if ! "$turbo_bin" run "$task" \
64
+ --dry=json --cache=remote:r --output-logs=none >"$probe_file" 2>&1; then
65
+ echo "Turbo Probe: CLI unavailable; using full install"
66
+ exit 0
67
+ fi
68
+
69
+ node - "$probe_file" >"$result_file" <<'NODE'
70
+ const fs = require('node:fs');
71
+ const file = process.argv[2];
72
+ const text = fs.readFileSync(file, 'utf8');
73
+ const start = text.indexOf('{');
74
+ if (start < 0) {
75
+ console.log('invalid-json');
76
+ process.exit(0);
77
+ }
78
+ let report;
79
+ try {
80
+ report = JSON.parse(text.slice(start));
81
+ } catch {
82
+ console.log('invalid-json');
83
+ process.exit(0);
84
+ }
85
+ const runnable = (report.tasks || []).filter((entry) =>
86
+ entry.command !== '<NONEXISTENT>' && entry.resolvedTaskDefinition?.cache !== false,
87
+ );
88
+ const hits = runnable.filter((entry) => entry.cache?.status === 'HIT').length;
89
+ const hit = runnable.length > 0 && hits === runnable.length;
90
+ console.log(`tasks=${report.tasks?.length || 0} runnable=${runnable.length} hits=${hits} hit=${hit}`);
91
+ NODE
92
+ probe_result="$(cat "$result_file")"
93
+ echo "Turbo Probe: $probe_result"
94
+ if ! grep -q 'hit=true' <<<"$probe_result"; then
95
+ echo "Turbo Probe: remote cache incomplete; using full install"
96
+ exit 0
97
+ fi
98
+
99
+ turbo_bin_dir="${turbo_bin%/*}"
100
+ echo "$turbo_bin_dir" >> "${GITHUB_PATH:-/dev/null}"
101
+ printf 'REPO_FOUNDRY_TURBO_REMOTE_ONLY=true\n' >> "${GITHUB_ENV:-/dev/null}"
102
+ set_output hit true
@@ -0,0 +1,7 @@
1
+ version: 1
2
+ template: code-foundry@latest
3
+ languages: typescript
4
+ features: all
5
+ package_manager: bun
6
+ release_type: node
7
+ npm_publish: true
@@ -0,0 +1,8 @@
1
+ # Repository-specific settings for the shared initializer and sync scripts.
2
+ version: 1
3
+ template: code-foundry@latest
4
+ languages: auto
5
+ features: all
6
+ package_manager: auto
7
+ release_type: auto
8
+ npm_publish: false
@@ -0,0 +1,166 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, staging]
6
+ pull_request:
7
+ branches: [staging]
8
+ workflow_dispatch:
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ env:
14
+ TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
15
+ TURBO_TEAM: ${{ vars.TURBO_TEAM }}
16
+ REPO_FOUNDRY_BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before || '' }}
17
+
18
+ concurrency:
19
+ group: ${{ github.workflow }}-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.event.pull_request.head.ref || github.ref_name }}
20
+ cancel-in-progress: true
21
+
22
+ jobs:
23
+ format:
24
+ name: Format
25
+ runs-on: ${{ vars.REPO_FOUNDRY_FAST_RUNNER || 'ubuntu-latest' }}
26
+ timeout-minutes: 15
27
+ steps:
28
+ - name: Checkout
29
+ uses: actions/checkout@v7
30
+ with:
31
+ fetch-depth: 2
32
+ - name: Detect
33
+ id: applicability
34
+ run: bash .github/scripts/ci.sh should_run format >> "$GITHUB_OUTPUT"
35
+ - name: Format Probe
36
+ id: format-probe
37
+ if: steps.applicability.outputs.applicable == 'true'
38
+ run: bash .github/scripts/format-fast-path.sh
39
+ - name: Setup
40
+ if: steps.applicability.outputs.applicable == 'true'
41
+ uses: ./.github/actions/setup
42
+ with:
43
+ install: ${{ steps.format-probe.outputs.hit != 'true' }}
44
+ install-python: false
45
+ install-rust: false
46
+ cache-build: false
47
+ # Warm the format cache on branch pushes without adding archive
48
+ # time to pull-request checks.
49
+ cache-save: ${{ github.event_name == 'push' }}
50
+ cache-format: true
51
+ task: format
52
+ - name: Format
53
+ if: steps.applicability.outputs.applicable == 'true'
54
+ id: format-fast
55
+ continue-on-error: ${{ steps.format-probe.outputs.hit == 'true' }}
56
+ run: bash .github/scripts/ci.sh format
57
+ - name: Setup Fallback
58
+ if: >-
59
+ steps.applicability.outputs.applicable == 'true' &&
60
+ steps.format-probe.outputs.hit == 'true' &&
61
+ steps.format-fast.outcome == 'failure'
62
+ uses: ./.github/actions/setup
63
+ with:
64
+ install: true
65
+ install-python: false
66
+ install-rust: false
67
+ cache-build: false
68
+ cache-save: false
69
+ cache-format: true
70
+ task: format
71
+ - name: Format Fallback
72
+ if: >-
73
+ steps.applicability.outputs.applicable == 'true' &&
74
+ steps.format-probe.outputs.hit == 'true' &&
75
+ steps.format-fast.outcome == 'failure'
76
+ run: bash .github/scripts/ci.sh format
77
+
78
+ lint:
79
+ name: Lint
80
+ runs-on: ${{ vars.REPO_FOUNDRY_FAST_RUNNER || 'ubuntu-latest' }}
81
+ timeout-minutes: 20
82
+ steps:
83
+ - name: Checkout
84
+ uses: actions/checkout@v7
85
+ with:
86
+ fetch-depth: 2
87
+ - name: Detect
88
+ id: applicability
89
+ run: bash .github/scripts/ci.sh should_run lint >> "$GITHUB_OUTPUT"
90
+ - name: Turbo Probe
91
+ id: turbo
92
+ if: steps.applicability.outputs.applicable == 'true'
93
+ run: bash .github/scripts/turbo-cache-probe.sh lint
94
+ - name: Setup
95
+ if: steps.applicability.outputs.applicable == 'true'
96
+ uses: ./.github/actions/setup
97
+ with:
98
+ install: ${{ steps.turbo.outputs.hit != 'true' }}
99
+ install-python: false
100
+ cache-build: false
101
+ # Warm the lint cache on branch pushes without adding archive time
102
+ # to pull-request checks.
103
+ cache-save: ${{ github.event_name == 'push' }}
104
+ cache-lint: true
105
+ task: lint
106
+ - name: Lint
107
+ if: steps.applicability.outputs.applicable == 'true'
108
+ run: bash .github/scripts/ci.sh lint
109
+
110
+ type-check:
111
+ name: Type-Check
112
+ runs-on: ubuntu-latest
113
+ timeout-minutes: 20
114
+ steps:
115
+ - name: Checkout
116
+ uses: actions/checkout@v7
117
+ with:
118
+ fetch-depth: 2
119
+ - name: Detect
120
+ id: applicability
121
+ run: bash .github/scripts/ci.sh should_run type_check >> "$GITHUB_OUTPUT"
122
+ - name: Turbo Probe
123
+ id: turbo
124
+ if: steps.applicability.outputs.applicable == 'true'
125
+ run: bash .github/scripts/turbo-cache-probe.sh type-check
126
+ - name: Setup
127
+ if: steps.applicability.outputs.applicable == 'true'
128
+ uses: ./.github/actions/setup
129
+ with:
130
+ install: ${{ steps.turbo.outputs.hit != 'true' }}
131
+ install-python: false
132
+ cache-build: ${{ steps.applicability.outputs.rust == 'true' || vars.REPO_FOUNDRY_CACHE_BUILD == 'true' }}
133
+ cache-save: ${{ github.event_name == 'push' }}
134
+ task: type_check
135
+ - name: Type-Check
136
+ if: steps.applicability.outputs.applicable == 'true'
137
+ run: bash .github/scripts/ci.sh type_check
138
+
139
+ build:
140
+ name: Build
141
+ runs-on: ubuntu-latest
142
+ timeout-minutes: 30
143
+ steps:
144
+ - name: Checkout
145
+ uses: actions/checkout@v7
146
+ with:
147
+ fetch-depth: 2
148
+ - name: Detect
149
+ id: applicability
150
+ run: bash .github/scripts/ci.sh should_run build >> "$GITHUB_OUTPUT"
151
+ - name: Turbo Probe
152
+ id: turbo
153
+ if: steps.applicability.outputs.applicable == 'true'
154
+ run: bash .github/scripts/turbo-cache-probe.sh build
155
+ - name: Setup
156
+ if: steps.applicability.outputs.applicable == 'true'
157
+ uses: ./.github/actions/setup
158
+ with:
159
+ install: ${{ steps.turbo.outputs.hit != 'true' }}
160
+ install-python: false
161
+ cache-build: ${{ steps.applicability.outputs.rust == 'true' || vars.REPO_FOUNDRY_CACHE_BUILD == 'true' }}
162
+ cache-save: ${{ github.event_name == 'push' }}
163
+ task: build
164
+ - name: Build
165
+ if: steps.applicability.outputs.applicable == 'true'
166
+ run: bash .github/scripts/ci.sh build
@@ -0,0 +1,160 @@
1
+ name: CodeQL
2
+
3
+ on:
4
+ push:
5
+ branches: [main, staging]
6
+ pull_request:
7
+ branches: [staging]
8
+ schedule:
9
+ - cron: '31 6 * * 1'
10
+ workflow_dispatch:
11
+
12
+ permissions:
13
+ actions: read
14
+ contents: read
15
+ packages: read
16
+ security-events: write
17
+
18
+ concurrency:
19
+ group: ${{ github.workflow }}-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.event.pull_request.head.ref || github.ref_name }}
20
+ cancel-in-progress: true
21
+
22
+ jobs:
23
+ detect:
24
+ name: Detect
25
+ runs-on: ubuntu-slim
26
+ timeout-minutes: 10
27
+ outputs:
28
+ languages: ${{ steps.languages.outputs.languages }}
29
+ code_security: ${{ steps.security.outputs.status }}
30
+ actions_available: ${{ steps.languages.outputs.actions_available }}
31
+ actions_changed: ${{ steps.languages.outputs.actions_changed }}
32
+ actions_build_mode: ${{ steps.languages.outputs.actions_build_mode }}
33
+ javascript_available: ${{ steps.languages.outputs.javascript_available }}
34
+ javascript_changed: ${{ steps.languages.outputs.javascript_changed }}
35
+ javascript_build_mode: ${{ steps.languages.outputs.javascript_build_mode }}
36
+ python_available: ${{ steps.languages.outputs.python_available }}
37
+ python_changed: ${{ steps.languages.outputs.python_changed }}
38
+ python_build_mode: ${{ steps.languages.outputs.python_build_mode }}
39
+ rust_available: ${{ steps.languages.outputs.rust_available }}
40
+ rust_changed: ${{ steps.languages.outputs.rust_changed }}
41
+ rust_build_mode: ${{ steps.languages.outputs.rust_build_mode }}
42
+ steps:
43
+ - name: Detect Code Security
44
+ id: security
45
+ env:
46
+ GH_TOKEN: ${{ github.token }}
47
+ run: |
48
+ status="disabled"
49
+ if [ "${{ github.event.repository.private }}" != "true" ]; then
50
+ status="enabled"
51
+ else
52
+ status="$(gh api "repos/${GITHUB_REPOSITORY}" --jq '.security_and_analysis.code_security.status // "disabled"' 2>/dev/null || printf 'disabled')"
53
+ fi
54
+ printf 'status=%s\n' "$status" >> "$GITHUB_OUTPUT"
55
+ - name: Checkout
56
+ if: ${{ !github.event.repository.private || steps.security.outputs.status == 'enabled' }}
57
+ uses: actions/checkout@v7
58
+ with:
59
+ fetch-depth: 2
60
+ # Detection only reads Git trees, paths, and small profile files;
61
+ # defer source blob transfer until the analyzer jobs.
62
+ filter: blob:none
63
+ sparse-checkout: |
64
+ .github
65
+ .mise.toml
66
+ mise.lock
67
+ package.json
68
+ bun.lock
69
+ bun.lockb
70
+ pnpm-lock.yaml
71
+ yarn.lock
72
+ package-lock.json
73
+ - id: languages
74
+ name: Detect languages
75
+ env:
76
+ CODEQL_BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
77
+ REPO_FOUNDRY_CODE_SECURITY: ${{ steps.security.outputs.status }}
78
+ REPO_FOUNDRY_PRIVATE: ${{ github.event.repository.private }}
79
+ run: |
80
+ if [ "$REPO_FOUNDRY_PRIVATE" = "true" ] && [ "$REPO_FOUNDRY_CODE_SECURITY" != "enabled" ]; then
81
+ echo 'languages=[]' >> "$GITHUB_OUTPUT"
82
+ else
83
+ bash .github/scripts/codeql-languages.sh
84
+ fi
85
+
86
+ analyze-actions:
87
+ name: Analyze (Actions)
88
+ needs: detect
89
+ if: >-
90
+ ( !github.event.repository.private || needs.detect.outputs.code_security == 'enabled' ) &&
91
+ needs.detect.outputs.actions_available == 'true' &&
92
+ needs.detect.outputs.actions_changed == 'true'
93
+ runs-on: ubuntu-latest
94
+ timeout-minutes: 30
95
+ steps:
96
+ - name: Checkout
97
+ uses: actions/checkout@v7
98
+ - name: Analyze
99
+ uses: ./.github/actions/codeql
100
+ with:
101
+ language: actions
102
+ build-mode: ${{ needs.detect.outputs.actions_build_mode }}
103
+ category: /language:actions
104
+
105
+ analyze-typescript:
106
+ name: Analyze (TypeScript)
107
+ needs: detect
108
+ if: >-
109
+ ( !github.event.repository.private || needs.detect.outputs.code_security == 'enabled' ) &&
110
+ needs.detect.outputs.javascript_available == 'true' &&
111
+ needs.detect.outputs.javascript_changed == 'true'
112
+ runs-on: ubuntu-latest
113
+ timeout-minutes: 30
114
+ steps:
115
+ - name: Checkout
116
+ uses: actions/checkout@v7
117
+ - name: Analyze
118
+ uses: ./.github/actions/codeql
119
+ with:
120
+ language: javascript-typescript
121
+ build-mode: ${{ needs.detect.outputs.javascript_build_mode }}
122
+ category: /language:javascript-typescript
123
+
124
+ analyze-python:
125
+ name: Analyze (Python)
126
+ needs: detect
127
+ if: >-
128
+ ( !github.event.repository.private || needs.detect.outputs.code_security == 'enabled' ) &&
129
+ needs.detect.outputs.python_available == 'true' &&
130
+ needs.detect.outputs.python_changed == 'true'
131
+ runs-on: ubuntu-latest
132
+ timeout-minutes: 30
133
+ steps:
134
+ - name: Checkout
135
+ uses: actions/checkout@v7
136
+ - name: Analyze
137
+ uses: ./.github/actions/codeql
138
+ with:
139
+ language: python
140
+ build-mode: ${{ needs.detect.outputs.python_build_mode }}
141
+ category: /language:python
142
+
143
+ analyze-rust:
144
+ name: Analyze (Rust)
145
+ needs: detect
146
+ if: >-
147
+ ( !github.event.repository.private || needs.detect.outputs.code_security == 'enabled' ) &&
148
+ needs.detect.outputs.rust_available == 'true' &&
149
+ needs.detect.outputs.rust_changed == 'true'
150
+ runs-on: ubuntu-latest
151
+ timeout-minutes: 30
152
+ steps:
153
+ - name: Checkout
154
+ uses: actions/checkout@v7
155
+ - name: Analyze
156
+ uses: ./.github/actions/codeql
157
+ with:
158
+ language: rust
159
+ build-mode: ${{ needs.detect.outputs.rust_build_mode }}
160
+ category: /language:rust
@@ -0,0 +1,59 @@
1
+ name: Draft PR
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - 'feat/*'
7
+ - 'fix/*'
8
+ - 'chore/*'
9
+ - 'refactor/*'
10
+ - 'docs/*'
11
+ - 'style/*'
12
+ - 'test/*'
13
+
14
+ permissions:
15
+ contents: read
16
+ pull-requests: write
17
+
18
+ env:
19
+ GH_TOKEN: ${{ github.token }}
20
+
21
+ jobs:
22
+ create-draft-pr:
23
+ name: Create
24
+ runs-on: ubuntu-slim
25
+ timeout-minutes: 10
26
+ concurrency:
27
+ group: ${{ github.workflow }}-${{ github.ref }}
28
+ cancel-in-progress: true
29
+ steps:
30
+ - name: Check
31
+ id: check-pr
32
+ run: |
33
+ BRANCH="${{ github.ref_name }}"
34
+ EXISTING=$(gh pr list --limit 1 --base staging --head "$BRANCH" --state open --json number,title 2>/dev/null | jq 'length')
35
+ echo "existing=$EXISTING" >> "$GITHUB_OUTPUT"
36
+
37
+ - name: Create
38
+ if: steps.check-pr.outputs.existing == '0'
39
+ run: |
40
+ BRANCH="${{ github.ref_name }}"
41
+ gh pr create \
42
+ --base staging \
43
+ --head "$BRANCH" \
44
+ --draft \
45
+ --title "[WIP] $BRANCH" \
46
+ --body "Draft PR created automatically by CI.
47
+
48
+ This PR is a draft and should not be merged until CI passes and a reviewer (or the Daily Review agent) marks it ready.
49
+
50
+ **CI Status:** Pending — this workflow triggered on push to \`$BRANCH\`."
51
+
52
+ - name: Mark ready
53
+ if: steps.check-pr.outputs.existing == '0'
54
+ run: |
55
+ BRANCH="${{ github.ref_name }}"
56
+ PR_NUMBER=$(gh pr list --limit 1 --base staging --head "$BRANCH" --state open --json number 2>/dev/null | jq '.[0].number')
57
+ if [ -n "$PR_NUMBER" ]; then
58
+ gh pr ready "$PR_NUMBER"
59
+ fi
@@ -0,0 +1,66 @@
1
+ name: Release PR
2
+
3
+ on:
4
+ push:
5
+ branches: [staging]
6
+
7
+ permissions:
8
+ contents: read
9
+ pull-requests: write
10
+
11
+ env:
12
+ GH_TOKEN: ${{ github.token }}
13
+
14
+ jobs:
15
+ create-release-pr:
16
+ name: Create
17
+ runs-on: ubuntu-slim
18
+ timeout-minutes: 10
19
+ concurrency:
20
+ group: ${{ github.workflow }}-${{ github.ref }}
21
+ cancel-in-progress: true
22
+ steps:
23
+ - name: Check
24
+ id: check-pr
25
+ run: |
26
+ EXISTING=$(gh pr list --limit 1 --base main --head staging --state open --json number 2>/dev/null | jq 'length')
27
+ echo "existing=$EXISTING" >> "$GITHUB_OUTPUT"
28
+
29
+ - name: Create
30
+ if: steps.check-pr.outputs.existing == '0'
31
+ run: |
32
+ DATE=$(date +%Y-%m-%d)
33
+ BODY_FILE="$RUNNER_TEMP/release-pr.md"
34
+ COMPARE_URL="repos/${GITHUB_REPOSITORY}/compare/main...staging"
35
+ COMPARE_FILE="$RUNNER_TEMP/release-compare.json"
36
+ gh api "$COMPARE_URL?per_page=100" > "$COMPARE_FILE"
37
+ TOTAL_COMMITS=$(jq -r '.total_commits // 0' "$COMPARE_FILE")
38
+ if [ "$TOTAL_COMMITS" -gt 100 ]; then
39
+ LAST_PAGE=$(( (TOTAL_COMMITS + 99) / 100 ))
40
+ gh api "$COMPARE_URL?per_page=100&page=$LAST_PAGE" > "$COMPARE_FILE"
41
+ fi
42
+ COMMITS=$(jq -r '.commits[] | "- \(.commit.message | split("\\n")[0]) (\(.sha[0:7]))"' "$COMPARE_FILE")
43
+ if [ -z "$COMMITS" ]; then
44
+ COMMITS='- No commits found since main.'
45
+ fi
46
+
47
+ cat > "$BODY_FILE" <<EOF
48
+ ## Release summary
49
+
50
+ Automated release PR from \`staging\` into \`main\` created on $DATE.
51
+
52
+ ### Changes since main
53
+
54
+ $COMMITS
55
+
56
+ ### Validation
57
+
58
+ - CI and security checks must pass before merge.
59
+ - Review migrations, configuration changes, and rollback notes.
60
+ EOF
61
+
62
+ gh pr create \
63
+ --base main \
64
+ --head staging \
65
+ --title "Release: staging -> main ($DATE)" \
66
+ --body-file "$BODY_FILE"
@@ -0,0 +1,103 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: write
10
+ issues: write
11
+ pull-requests: write
12
+
13
+ jobs:
14
+ release:
15
+ name: Release / Version
16
+ runs-on: ubuntu-slim
17
+ timeout-minutes: 20
18
+ concurrency:
19
+ group: ${{ github.workflow }}-${{ github.ref }}
20
+ cancel-in-progress: false
21
+ outputs:
22
+ release_created: ${{ steps.release.outputs.release_created || 'false' }}
23
+ tag_name: ${{ steps.release.outputs.tag_name }}
24
+ npm_publish: ${{ steps.profile.outputs.npm_publish }}
25
+ steps:
26
+ - name: Checkout
27
+ uses: actions/checkout@v7
28
+ with:
29
+ # Release Please reads release metadata and manifests; defer source
30
+ # blob transfer until a package file is actually needed.
31
+ filter: blob:none
32
+ - name: Detect release profile
33
+ id: profile
34
+ shell: bash
35
+ run: |
36
+ release_type="$(awk -F': ' '/^release_type:/ {print $2; exit}' .github/template.yml 2>/dev/null || true)"
37
+ npm_publish="$(awk -F': ' '/^npm_publish:/ {print $2; exit}' .github/template.yml 2>/dev/null || true)"
38
+ [ -n "$release_type" ] || release_type=auto
39
+ [ -n "$npm_publish" ] || npm_publish=false
40
+
41
+ if [ "$release_type" = auto ]; then
42
+ if [ -f package.json ]; then release_type=node
43
+ elif [ -f pyproject.toml ]; then release_type=python
44
+ elif [ -f Cargo.toml ]; then release_type=rust
45
+ elif [ -f version.txt ]; then release_type=simple
46
+ else release_type=none
47
+ fi
48
+ fi
49
+
50
+ case "$release_type" in
51
+ node|python|rust|simple) ;;
52
+ none) npm_publish=false ;;
53
+ *) echo "Unsupported release_type: $release_type" >&2; exit 2 ;;
54
+ esac
55
+ if [ ! -f package.json ]; then npm_publish=false; fi
56
+ printf 'release_type=%s\n' "$release_type" >> "$GITHUB_OUTPUT"
57
+ printf 'npm_publish=%s\n' "$npm_publish" >> "$GITHUB_OUTPUT"
58
+ - name: Release Please
59
+ id: release
60
+ if: steps.profile.outputs.release_type != 'none'
61
+ uses: googleapis/release-please-action@v5
62
+ with:
63
+ # Use a PAT or GitHub App token when GITHUB_TOKEN cannot create PRs.
64
+ token: ${{ secrets.RELEASE_PLEASE_TOKEN || github.token }}
65
+ config-file: release-please-config.json
66
+ release-type: ${{ steps.profile.outputs.release_type }}
67
+ bump-minor-pre-major: true
68
+
69
+ npm:
70
+ name: Release / Publish npm
71
+ needs: release
72
+ if: needs.release.outputs.release_created == 'true' && needs.release.outputs.npm_publish == 'true'
73
+ runs-on: ubuntu-slim
74
+ timeout-minutes: 15
75
+ permissions:
76
+ contents: read
77
+ id-token: write
78
+ env:
79
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
80
+ steps:
81
+ - name: Checkout release
82
+ uses: actions/checkout@v7
83
+ with:
84
+ ref: ${{ needs.release.outputs.tag_name }}
85
+ - name: Setup Node (trusted)
86
+ if: env.NPM_TOKEN == ''
87
+ uses: actions/setup-node@v5
88
+ with:
89
+ node-version: 24
90
+ - name: Setup Node (token)
91
+ if: env.NPM_TOKEN != ''
92
+ uses: actions/setup-node@v5
93
+ with:
94
+ node-version: 24
95
+ registry-url: https://registry.npmjs.org
96
+ - name: Publish npm with token
97
+ if: env.NPM_TOKEN != ''
98
+ env:
99
+ NODE_AUTH_TOKEN: ${{ env.NPM_TOKEN }}
100
+ run: npm publish --provenance --access public
101
+ - name: Publish npm with trusted publishing
102
+ if: env.NPM_TOKEN == ''
103
+ run: npm publish --provenance --access public