Skip to content

Commit e7cdc30

Browse files
[8.19] Automate elasticsearch-specification bumps on request-converter-dotnet publish (#8940) (#8941)
Co-authored-by: Florian Bernd <git@flobernd.de>
1 parent 3301a21 commit e7cdc30

2 files changed

Lines changed: 181 additions & 0 deletions

File tree

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
name: Bump elasticsearch-specification
2+
3+
# Reusable workflow that opens PRs in elastic/elasticsearch-specification bumping a
4+
# converter package pin in docs/examples/package.json. Called from the npm publish
5+
# workflow after a successful publish; also dispatchable manually for testing.
6+
#
7+
# Targets the spec branch matching the published major.minor, and additionally
8+
# targets `main` when the publish moved the `latest` dist-tag.
9+
#
10+
# Uses an ephemeral Vault (ci-prod) token fetched via OIDC rather than the
11+
# default GITHUB_TOKEN: bump PRs target a different repo and must trigger its
12+
# CI. Permissions (contents:write, pull_requests:write on
13+
# elasticsearch-specification) are gated by the spec-bump token policies in
14+
# catalog-info. Requires id-token:write on the caller.
15+
16+
on:
17+
workflow_call:
18+
inputs:
19+
version:
20+
description: 'Published package version, e.g. 9.4.2'
21+
required: true
22+
type: string
23+
branch:
24+
description: 'Source major.minor branch, e.g. 9.4 (matches the spec branch to target)'
25+
required: true
26+
type: string
27+
dist-tag:
28+
description: 'npm dist-tag the version was published with: "latest" or "latest-<branch>"'
29+
required: true
30+
type: string
31+
package:
32+
description: 'npm package to bump in docs/examples/package.json'
33+
required: true
34+
type: string
35+
dry-run:
36+
description: 'Log the planned PRs without creating them'
37+
required: false
38+
default: false
39+
type: boolean
40+
workflow_dispatch:
41+
inputs:
42+
version:
43+
description: 'Published package version, e.g. 9.4.2'
44+
required: true
45+
branch:
46+
description: 'Source major.minor branch, e.g. 9.4'
47+
required: true
48+
dist-tag:
49+
description: 'npm dist-tag: "latest" or "latest-<branch>"'
50+
required: true
51+
package:
52+
description: 'npm package to bump'
53+
required: true
54+
dry-run:
55+
description: 'Log the planned PRs without creating them'
56+
required: false
57+
default: true
58+
59+
permissions:
60+
contents: read
61+
id-token: write
62+
63+
jobs:
64+
bump:
65+
runs-on: ubuntu-latest
66+
steps:
67+
- name: Fetch ephemeral GitHub token
68+
id: fetch-token
69+
uses: elastic/ci-gh-actions/fetch-github-token@2feb1c6f5086cf8e06f61ef35e275abed3456f7b # v1.5.3
70+
with:
71+
vault-instance: "ci-prod"
72+
- name: Open bump PRs in elasticsearch-specification
73+
uses: actions/github-script@v7
74+
env:
75+
SPEC_PKG: ${{ inputs.package }}
76+
SPEC_VERSION: ${{ inputs.version }}
77+
SPEC_BRANCH: ${{ inputs.branch }}
78+
SPEC_DIST_TAG: ${{ inputs.dist-tag }}
79+
SPEC_DRY_RUN: ${{ inputs.dry-run }}
80+
with:
81+
github-token: ${{ steps.fetch-token.outputs.token }}
82+
script: |
83+
const owner = 'elastic';
84+
const repo = 'elasticsearch-specification';
85+
const pkg = process.env.SPEC_PKG;
86+
const version = process.env.SPEC_VERSION;
87+
const branch = process.env.SPEC_BRANCH;
88+
const distTag = process.env.SPEC_DIST_TAG;
89+
const dryRun = String(process.env.SPEC_DRY_RUN) === 'true';
90+
const unscoped = pkg.replace(/^@[^/]+\//, '');
91+
const pin = `~${version}`;
92+
const path = 'docs/examples/package.json';
93+
94+
// Always target the matching major.minor branch; also target main when
95+
// the publish moved the "latest" dist-tag.
96+
const targets = [branch];
97+
if (distTag === 'latest') targets.push('main');
98+
99+
core.info(`Bumping ${pkg} to ${pin} on ${owner}/${repo}: ${targets.join(', ')}${dryRun ? ' (dry-run)' : ''}`);
100+
101+
for (const target of targets) {
102+
const bumpBranch = `bump/${unscoped}-${version}-${target}`;
103+
core.info(`\n--- ${target} -> ${bumpBranch} ---`);
104+
try {
105+
// Skip if the target spec branch doesn't exist (no branch for this major.minor).
106+
let baseSha;
107+
try {
108+
const ref = await github.rest.git.getRef({ owner, repo, ref: `heads/${target}` });
109+
baseSha = ref.object.sha;
110+
} catch (e) {
111+
core.warning(`Branch '${target}' not found in ${owner}/${repo}; skipping.`);
112+
continue;
113+
}
114+
115+
// Skip if an open PR for this bump branch already exists (idempotent).
116+
const existing = await github.rest.pulls.list({
117+
owner, repo, state: 'open', head: `${owner}:${bumpBranch}`, per_page: 1
118+
});
119+
if (existing.data.length > 0) {
120+
core.info(`Open PR already exists (${existing.data[0].html_url}); skipping.`);
121+
continue;
122+
}
123+
124+
// Read the current package.json from the target branch.
125+
const file = await github.rest.repos.getContent({ owner, repo, path, ref: target });
126+
const content = Buffer.from(file.data.content.replace(/\s/g, ''), 'base64').toString('utf8');
127+
const pkgJson = JSON.parse(content);
128+
const current = pkgJson.dependencies && pkgJson.dependencies[pkg];
129+
if (current === pin) {
130+
core.info(`${pkg} already at ${pin} on ${target}; skipping.`);
131+
continue;
132+
}
133+
pkgJson.dependencies = pkgJson.dependencies || {};
134+
pkgJson.dependencies[pkg] = pin;
135+
const newContent = Buffer.from(JSON.stringify(pkgJson, null, 2) + '\n', 'utf8').toString('base64');
136+
137+
if (dryRun) {
138+
core.info(`[dry-run] Would bump ${pkg}: ${current || '(missing)'} -> ${pin} on ${target}; open PR from ${bumpBranch}.`);
139+
continue;
140+
}
141+
142+
// Create the bump branch and commit the updated package.json.
143+
await github.rest.git.createRef({ owner, repo, ref: `refs/heads/${bumpBranch}`, sha: baseSha });
144+
await github.request('PUT /repos/{owner}/{repo}/contents/{path}', {
145+
owner, repo, path,
146+
message: `Bump ${pkg} to ${version} (${target})`,
147+
content: newContent, branch: bumpBranch, sha: file.data.sha
148+
});
149+
const pr = await github.rest.pulls.create({
150+
owner, repo, head: bumpBranch, base: target,
151+
title: `Bump ${pkg} to ${version}`,
152+
body: [
153+
`Automated bump of \`${pkg}\` to \`${pin}\` in \`${path}\`.`,
154+
'',
155+
`Triggered by the publish of \`${pkg}@${version}\` (dist-tag \`${distTag}\`, source branch \`${branch}\`).`,
156+
'',
157+
'The lockfile is intentionally left unchanged; CI `npm install` reconciles the new version.',
158+
].join('\n')
159+
});
160+
core.info(`Opened PR: ${pr.data.html_url}`);
161+
} catch (e) {
162+
core.error(`Failed for target ${target}: ${e.message}`);
163+
}
164+
}

.github/workflows/request-converter-npm-publish.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ permissions:
1616
jobs:
1717
publish:
1818
runs-on: ubuntu-latest
19+
outputs:
20+
version: ${{ steps.version.outputs.version }}
21+
branch: ${{ steps.branch.outputs.branch }}
22+
dist-tag: ${{ steps.dist-tag.outputs.tag }}
1923
steps:
2024
- name: Resolve branch
2125
id: branch
@@ -91,3 +95,16 @@ jobs:
9195
echo "Publishing ${NEW_VERSION} with dist-tag ${TAG} (current latest: ${CURRENT_LATEST})"
9296
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
9397
- run: npm publish .artifacts/npm/request-converter-dotnet --access public --provenance --tag "${{ steps.dist-tag.outputs.tag }}"
98+
99+
# After a successful publish, open a bump PR in elasticsearch-specification targeting
100+
# the matching major.minor branch (and main when this publish moved the "latest" tag).
101+
bump-spec:
102+
needs: publish
103+
if: ${{ needs.publish.result == 'success' }}
104+
uses: ./.github/workflows/bump-elasticsearch-specification.yml
105+
with:
106+
version: ${{ needs.publish.outputs.version }}
107+
branch: ${{ needs.publish.outputs.branch }}
108+
dist-tag: ${{ needs.publish.outputs.dist-tag }}
109+
package: "@elastic/request-converter-dotnet"
110+
dry-run: false

0 commit comments

Comments
 (0)