Skip to content

Commit c7e1f49

Browse files
authored
[codex] Automate release changelogs and locked tooling (#39)
1 parent 3e62c57 commit c7e1f49

11 files changed

Lines changed: 567 additions & 54 deletions

File tree

.github/workflows/ci.yml

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,14 @@ jobs:
2525
timeout-minutes: 20
2626
steps:
2727
- name: Check out repository
28-
uses: actions/checkout@v6
28+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
29+
with:
30+
persist-credentials: false
2931

3032
- name: Set up mise
31-
uses: jdx/mise-action@v3
33+
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
34+
with:
35+
install_args: --locked
3236

3337
- name: Install CI dependencies
3438
run: mise run bootstrap-ci
@@ -39,6 +43,9 @@ jobs:
3943
- name: Check formatting
4044
run: mise run format-check
4145

46+
- name: Workflow lint
47+
run: mise run workflow-lint
48+
4249
- name: Lint
4350
run: mise run lint
4451

@@ -64,10 +71,16 @@ jobs:
6471
timeout-minutes: 20
6572
steps:
6673
- name: Check out repository
67-
uses: actions/checkout@v6
74+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
75+
with:
76+
persist-credentials: false
6877

6978
- name: Set up mise
70-
uses: jdx/mise-action@v3
79+
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
80+
with:
81+
# The macOS job does not run workflow linting or Communique, so avoid
82+
# installing release-only tools that are only locked for Linux/macOS arm64.
83+
install_args: --locked node python
7184

7285
- name: Install CI dependencies
7386
run: mise run bootstrap-ci
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
name: Release Changelog
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened, ready_for_review]
6+
branches:
7+
- main
8+
paths:
9+
- package.json
10+
- package-lock.json
11+
- CHANGELOG.md
12+
- communique.toml
13+
- mise.lock
14+
- mise.toml
15+
- .github/workflows/release-changelog.yml
16+
17+
permissions:
18+
actions: write
19+
contents: write
20+
issues: read
21+
pull-requests: read
22+
23+
jobs:
24+
update-changelog:
25+
if: >-
26+
github.event.pull_request.head.repo.full_name == github.repository &&
27+
startsWith(github.head_ref, 'release/')
28+
runs-on: ubuntu-latest
29+
timeout-minutes: 20
30+
steps:
31+
- name: Check out release branch
32+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
33+
with:
34+
ref: ${{ github.head_ref }}
35+
fetch-depth: 0
36+
persist-credentials: false
37+
38+
- name: Set up mise
39+
uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3.6.3
40+
with:
41+
install_args: --locked
42+
43+
- name: Resolve release metadata
44+
id: release-metadata
45+
shell: bash
46+
env:
47+
BASE_REF: ${{ github.base_ref }}
48+
run: |
49+
set -euo pipefail
50+
git fetch --no-tags origin \
51+
"refs/heads/$BASE_REF:refs/remotes/origin/$BASE_REF"
52+
53+
node --input-type=module <<'EOF'
54+
import assert from 'node:assert/strict';
55+
import { appendFileSync, readFileSync } from 'node:fs';
56+
import { execFileSync } from 'node:child_process';
57+
58+
const packageJson = JSON.parse(readFileSync('package.json', 'utf8'));
59+
assert(packageJson !== null && typeof packageJson === 'object', 'package.json must parse to an object');
60+
assert(typeof packageJson.version === 'string' && packageJson.version.length > 0, 'package.json version must not be empty');
61+
62+
const basePackageJsonText = execFileSync(
63+
'git',
64+
['show', `refs/remotes/origin/${process.env.BASE_REF}:package.json`],
65+
{ encoding: 'utf8' },
66+
);
67+
const basePackageJson = JSON.parse(basePackageJsonText);
68+
assert(basePackageJson !== null && typeof basePackageJson === 'object', 'base package.json must parse to an object');
69+
70+
const packageVersion = packageJson.version;
71+
const baseVersion = typeof basePackageJson.version === 'string' ? basePackageJson.version : '';
72+
const releaseTag = `v${packageVersion}`;
73+
const shouldRun = packageVersion !== baseVersion;
74+
75+
for (const [key, value] of Object.entries({
76+
package_version: packageVersion,
77+
release_tag: releaseTag,
78+
should_run: String(shouldRun),
79+
})) {
80+
assert(typeof value === 'string' && value.length > 0, `${key} must not be empty`);
81+
appendFileSync(process.env.GITHUB_OUTPUT, `${key}=${value}\n`);
82+
}
83+
EOF
84+
85+
- name: Check existing changelog entry
86+
id: changelog-entry
87+
if: steps.release-metadata.outputs.should_run == 'true'
88+
shell: bash
89+
env:
90+
PACKAGE_VERSION: ${{ steps.release-metadata.outputs.package_version }}
91+
RELEASE_TAG: ${{ steps.release-metadata.outputs.release_tag }}
92+
run: |
93+
set -euo pipefail
94+
if [[ -f CHANGELOG.md ]] && { grep -Fq "## [${RELEASE_TAG}]" CHANGELOG.md || grep -Fq "## [${PACKAGE_VERSION}]" CHANGELOG.md; }; then
95+
echo 'exists=true' >> "$GITHUB_OUTPUT"
96+
else
97+
echo 'exists=false' >> "$GITHUB_OUTPUT"
98+
fi
99+
100+
- name: Update CHANGELOG.md with Communique
101+
if: >-
102+
steps.release-metadata.outputs.should_run == 'true' &&
103+
steps.changelog-entry.outputs.exists == 'false'
104+
shell: bash
105+
env:
106+
GITHUB_TOKEN: ${{ github.token }}
107+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
108+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
109+
COMMUNIQUE_MODEL: ${{ vars.COMMUNIQUE_MODEL }}
110+
RELEASE_TAG: ${{ steps.release-metadata.outputs.release_tag }}
111+
run: |
112+
set -euo pipefail
113+
if [[ -z "${ANTHROPIC_API_KEY:-}" && -z "${OPENAI_API_KEY:-}" ]]; then
114+
echo 'ANTHROPIC_API_KEY or OPENAI_API_KEY is required to generate release changelog entries with Communique.' >&2
115+
exit 1
116+
fi
117+
if [[ -z "${ANTHROPIC_API_KEY:-}" && -z "${COMMUNIQUE_MODEL:-}" ]]; then
118+
echo 'Set COMMUNIQUE_MODEL when using OPENAI_API_KEY so Communique can select an OpenAI-compatible model.' >&2
119+
exit 1
120+
fi
121+
122+
communique_args=()
123+
if [[ -n "${COMMUNIQUE_MODEL:-}" ]]; then
124+
communique_args+=(--model "$COMMUNIQUE_MODEL")
125+
fi
126+
127+
communique generate "$RELEASE_TAG" \
128+
--changelog \
129+
--repo "$GITHUB_REPOSITORY" \
130+
"${communique_args[@]}"
131+
132+
- name: Commit changelog update
133+
id: commit_changelog
134+
if: >-
135+
steps.release-metadata.outputs.should_run == 'true' &&
136+
steps.changelog-entry.outputs.exists == 'false'
137+
shell: bash
138+
env:
139+
GH_TOKEN: ${{ github.token }}
140+
RELEASE_TAG: ${{ steps.release-metadata.outputs.release_tag }}
141+
run: |
142+
set -euo pipefail
143+
if git diff --quiet -- CHANGELOG.md; then
144+
echo 'CHANGELOG.md is already up to date.'
145+
echo 'pushed=false' >> "$GITHUB_OUTPUT"
146+
exit 0
147+
fi
148+
149+
git config user.name 'github-actions[bot]'
150+
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
151+
git add CHANGELOG.md
152+
git commit -m "docs: update changelog for ${RELEASE_TAG}"
153+
git -c "http.https://github.com/.extraheader=AUTHORIZATION: bearer ${GH_TOKEN}" \
154+
push "https://github.com/${GITHUB_REPOSITORY}.git" "HEAD:${GITHUB_HEAD_REF}"
155+
echo 'pushed=true' >> "$GITHUB_OUTPUT"
156+
157+
- name: Dispatch follow-up checks
158+
if: steps.commit_changelog.outputs.pushed == 'true'
159+
shell: bash
160+
env:
161+
GH_TOKEN: ${{ github.token }}
162+
run: |
163+
set -euo pipefail
164+
gh workflow run ci.yml --ref "$GITHUB_HEAD_REF"
165+
gh workflow run validate-skills.yml --ref "$GITHUB_HEAD_REF"

0 commit comments

Comments
 (0)