diff --git a/.github/actions/resolve-release-branch/README.md b/.github/actions/resolve-release-branch/README.md index 804ca8d..3eeb31b 100644 --- a/.github/actions/resolve-release-branch/README.md +++ b/.github/actions/resolve-release-branch/README.md @@ -29,6 +29,7 @@ same project is refused rather than silently rewriting `main`. | Input | Description | Required | Default | |-------|-------------|----------|---------| | `repo` | Full repository path (e.g. `spring-cloud/spring-cloud-config`) | Yes | | +| `internal` | Resolve `..x-internal` instead of `..x`. No `main` fallback; a missing branch is `branch-not-found`. | No | `false` | | `version` | The version whose line is wanted. Any qualifier is stripped first, so `5.0.4-SNAPSHOT`, `5.1.0-INTERNAL-SNAPSHOT`, `5.1.0-M1` and `5.0.3` all resolve the same way. | Yes | | | `commercial` | Repository is a commercial one, so there is no `main` to fall back to | No | `false` | | `token` | Token with read access to the repository | Yes | | diff --git a/.github/actions/resolve-release-branch/action.yml b/.github/actions/resolve-release-branch/action.yml index d9b9309..a942c8b 100644 --- a/.github/actions/resolve-release-branch/action.yml +++ b/.github/actions/resolve-release-branch/action.yml @@ -1,7 +1,8 @@ name: 'Resolve Release Branch' description: > Resolves the maintenance branch a given project version belongs to: ..x - when it exists, otherwise main for OSS repositories whose main is on that same line. + when it exists, otherwise main for OSS repositories whose main is on that same line. With + internal=true it resolves ..x-internal instead, with no fallback. inputs: repo: @@ -20,6 +21,13 @@ inputs: reported rather than guessed at. required: false default: 'false' + internal: + description: > + When true, resolve the long-lived ..x-internal branch of a commercial + repository rather than ..x. A missing branch is reported, never replaced + by main. + required: false + default: 'false' token: description: 'GitHub token with read access to the repository' required: true @@ -48,6 +56,7 @@ runs: REPO: ${{ inputs.repo }} VERSION: ${{ inputs.version }} COMMERCIAL: ${{ inputs.commercial }} + INTERNAL: ${{ inputs.internal }} run: | node - << 'JSEOF' const fs = require('fs'); @@ -56,6 +65,7 @@ runs: const repo = process.env.REPO; const version = process.env.VERSION; const commercial = process.env.COMMERCIAL === 'true'; + const internal = process.env.INTERNAL === 'true'; const out = process.env.GITHUB_OUTPUT; const emit = (k, v) => fs.appendFileSync(out, `${k}=${v}\n`); @@ -78,7 +88,8 @@ runs: stop('version-mismatch', `ERROR: '${version}' is not a numeric version, so no branch can be derived from it.`); } - const target = parts.slice(0, -1).join('.') + '.x'; + const line = parts.slice(0, -1).join('.') + '.x'; + const target = internal ? `${line}-internal` : line; const branchExists = branch => { try { @@ -96,6 +107,12 @@ runs: process.exit(0); } + // An -internal branch is a copy of the OSS branch kept for the commercial repos; main is + // never a stand-in for it. + if (internal) { + stop('branch-not-found', `ERROR: ${repo} has no ${target} branch. Skipping this project.`); + } + // Commercial repos have no main branch at all - spring-cloud-config-commercial's // default is 4.3.x - so there is no sane fallback to make. if (commercial) { diff --git a/.github/workflows/README-post-release.md b/.github/workflows/README-post-release.md index b4ec640..3bb223f 100644 --- a/.github/workflows/README-post-release.md +++ b/.github/workflows/README-post-release.md @@ -717,7 +717,7 @@ Notes on this: ## Notes - **Milestone lookups are paginated, and that is not optional.** There is no "get a milestone by title" endpoint, so every milestone check here lists them — and several of these repositories are well past a hundred milestones (`spring-cloud-release` has 167, `spring-cloud-config` 154, `spring-cloud-commons` 151). The list API returns them **sorted by due date ascending**, so an unpaginated `per_page=100` read drops exactly the newest ones: the release just made and the one after it. A run against `2025.1.3` reported `not-found` for five repositories whose milestones were plainly there. Every lookup now uses `--paginate` — `--slurp` in the Node steps, since gh refuses `--slurp` together with `--jq`, and `jq -s add` in the shell ones. -- **`spring-boot` is in every properties file but is not a Spring Cloud repository**, so it is excluded from tag checks, milestones, releases and branch updates. It *is* bumped in the snapshot properties file, matching the existing files. +- **`spring-boot` and `spring-vault` are in every properties file but are not Spring Cloud repositories**, so they are excluded from tag checks, milestones, releases and branch updates. They *are* bumped in the snapshot properties file, matching the existing files. - Pre-release qualifiers (`-M1`, `-RC1`) are rejected rather than half-handled — bumping the patch of `2025.1.0-RC1` produces a version nobody wants, and post-release chores are not run for milestones or release candidates. - `update-project-versions` is called with `release-train-version` rather than an explicit versions map, because only that path applies `project-version-substitutions` (which maps `spring-cloud-dependencies-parent` → `spring-cloud-build`, `verifierVersion` → `spring-cloud-contract`, and so on). That path resolves over `raw.githubusercontent.com`, which is CDN-cached, so after committing the snapshot file the workflow waits for the raw URL to serve it before any project is updated. If the CDN never catches up, version updates are skipped rather than applied from a stale file, and the run can simply be repeated. - Tag existence is checked with `git/matching-refs` and an exact comparison, not a plain `git/refs/tags/` lookup, which would also prefix-match `v5.0.20` when asked for `v5.0.2`. diff --git a/.github/workflows/README-update-versions.md b/.github/workflows/README-update-versions.md index 39dadef..0cf7daf 100644 --- a/.github/workflows/README-update-versions.md +++ b/.github/workflows/README-update-versions.md @@ -37,7 +37,7 @@ For the normal end-of-release path, run [post-release](README-post-release.md) | `dry_run` | Clone, update, and diff without pushing | No | boolean (default: `true`) | | `token` | Token with write access to all target repos. Falls back to `GH_ACTIONS_REPO_TOKEN`. | No | string | -`spring-boot` is in the properties file so that every project picks up its version, but it is not a Spring Cloud repository — it is never pushed to, and naming it in `projects` is an error. +`spring-boot` and `spring-vault` are in the properties file so that every project picks up their versions, but they are not Spring Cloud repositories — they are never pushed to, and naming either in `projects` is an error. ## OSS or commercial, never both @@ -54,7 +54,11 @@ If that branch does not exist: - **commercial** — the project is skipped. Commercial repos have no `main` to fall back to (`spring-cloud-config-commercial`'s default branch is `4.3.x`). - **OSS** — falls back to `main`, but only after reading `pom.xml` on `main` and confirming it is on the same `major.minor` line. Otherwise the project is skipped with `version-mismatch` rather than bumping an unrelated major to these versions. -**There is no input to override this.** Between those two rules every branch the workflow targets is covered: the only names in [`projects.json`](../../config/projects.json) that are not `..x` are `main`, which the fallback handles, and the `-internal` branches, which this never targets — those are rebased from the OSS branches it does update. An override input would therefore have no correct use, and would be the one way to push a train's versions onto a branch they do not belong on, since a branch given by hand cannot be checked against the version the way a derived one can. +### `-INTERNAL-SNAPSHOT` trains + +A `release_train_version` such as `2025.1.4-INTERNAL-SNAPSHOT` targets the long-lived `..x-internal` branches of the **commercial** repositories instead (`5.1.0-INTERNAL-SNAPSHOT` → `5.1.x-internal`). There is no fallback: a project without that branch is skipped as `branch-not-found`. The run fails early if `commercial` is not set (or the project names lack `-commercial`), since OSS repos have no `-internal` branches. The properties file read is `2025_1_4-internal-snapshot.properties` on `jenkins-releaser-config`, which must already exist. + +**There is no input to override this.** Between those two rules every branch the workflow targets is covered: the only names in [`projects.json`](../../config/projects.json) that are not `..x` are `main`, which the fallback handles, and the `-internal` branches, which are reached only through an `-INTERNAL` train (above). An override input would therefore have no correct use, and would be the one way to push a train's versions onto a branch they do not belong on, since a branch given by hand cannot be checked against the version the way a derived one can. ## The raw wait diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml index aa968c6..800f1cc 100644 --- a/.github/workflows/post-release.yml +++ b/.github/workflows/post-release.yml @@ -249,9 +249,10 @@ jobs: } console.log(`Found ${entries.length} version entries.`); - // spring-boot lives in the properties file but is not a Spring Cloud repository, so it - // has no tag, milestone or release of ours. It is still bumped in the snapshot file. - const NON_REPO_KEYS = new Set(['spring-boot']); + // spring-boot and spring-vault live in the properties file but are not Spring Cloud + // repositories, so they have no tag, milestone or release of ours. They are still bumped + // in the snapshot file. + const NON_REPO_KEYS = new Set(['spring-boot', 'spring-vault']); let wanted = null; if (names.length) { @@ -1630,7 +1631,7 @@ jobs: // ones without access - a row of links that 404 for the audience is worse than // no links at all. The module and its version are the useful part either way. const included = entries - .filter(e => e.key !== 'spring-boot' && e.key !== 'spring-cloud-release') + .filter(e => !['spring-boot', 'spring-vault', 'spring-cloud-release'].includes(e.key)) .map(e => commercial ? ` - ${titleCase(e.key)} \`${e.version}\`` : ` - ${titleCase(e.key)} \`${e.version}\` ` + @@ -1848,7 +1849,7 @@ jobs: // Every project in the train, for the OSS site: it documents the versions currently // being documented, and even a project that did not move needs its snapshot entry // following the new snapshot train. - const projects = entries.filter(e => e.key !== 'spring-boot'); + const projects = entries.filter(e => !['spring-boot', 'spring-vault'].includes(e.key)); // For the commercial site, only the projects with a tag of their own in the // commercial repo. An oss-fallback entry carries the plain OSS version because that diff --git a/.github/workflows/setup-next-release-train.yml b/.github/workflows/setup-next-release-train.yml index c9078a1..c894e4d 100644 --- a/.github/workflows/setup-next-release-train.yml +++ b/.github/workflows/setup-next-release-train.yml @@ -151,9 +151,9 @@ jobs: } console.log(`Found ${entries.length} version entries.`); - // spring-boot is in the properties file so that every project picks up its version, but - // it is not a Spring Cloud repository and nothing is pushed to it. - const NON_REPO_KEYS = new Set(['spring-boot']); + // spring-boot and spring-vault are in the properties file so that every project picks up + // their versions, but they are not Spring Cloud repositories and nothing is pushed to them. + const NON_REPO_KEYS = new Set(['spring-boot', 'spring-vault']); let wanted = null; if (names.length) { diff --git a/.github/workflows/update-versions.yml b/.github/workflows/update-versions.yml index d876a52..873a177 100644 --- a/.github/workflows/update-versions.yml +++ b/.github/workflows/update-versions.yml @@ -32,7 +32,7 @@ on: workflow_dispatch: inputs: release_train_version: - description: 'Release train version whose versions should be applied (e.g. 2025.1.3-SNAPSHOT, or 2025.1.2). Must have a matching properties file on the jenkins-releaser-config branch.' + description: 'Release train version whose versions should be applied (e.g. 2025.1.3-SNAPSHOT, 2025.1.2, or 2025.1.4-INTERNAL-SNAPSHOT for the -internal branches of the commercial repos). Must have a matching properties file on the jenkins-releaser-config branch.' required: true type: string commercial: @@ -69,6 +69,7 @@ jobs: commercial: ${{ steps.parse.outputs.commercial }} release-repo: ${{ steps.parse.outputs.release-repo }} props-file: ${{ steps.parse.outputs.props-file }} + internal: ${{ steps.parse.outputs.internal }} steps: # Only so the inline scripts below can require .github/scripts/. - uses: actions/checkout@v4 @@ -103,13 +104,19 @@ jobs: const fail = (...msg) => { for (const m of msg) console.error(m); process.exit(1); }; // 3 or 4 numeric segments, optionally with a qualifier: 2025.1.3, 2025.1.3-SNAPSHOT, - // 2025.1.0-RC1, 2025.1.2.1. Anything else would resolve to a properties file name - // that cannot exist, so it is rejected here rather than as a confusing 404. - if (!/^\d+(\.\d+){2,3}(-[A-Za-z][\w.]*)?$/.test(trainVersion)) { + // 2025.1.0-RC1, 2025.1.2.1, 2025.1.4-INTERNAL-SNAPSHOT (the qualifier may itself + // contain hyphens). Anything else would resolve to a properties file name that cannot + // exist, so it is rejected here rather than as a confusing 404. + if (!/^\d+(\.\d+){2,3}(-[A-Za-z][\w.]*)*$/.test(trainVersion)) { fail(`ERROR: release_train_version must be 3 or 4 numeric segments with an optional ` + - `qualifier (e.g. 2025.1.3-SNAPSHOT or 2025.1.2); got '${trainVersion}'.`); + `qualifier (e.g. 2025.1.3-SNAPSHOT, 2025.1.2 or 2025.1.4-INTERNAL-SNAPSHOT); ` + + `got '${trainVersion}'.`); } + // An -INTERNAL train addresses the ..x-internal branches, which exist + // only in the commercial repositories. + const internal = /-INTERNAL(-|$)/i.test(trainVersion); + // This job must read exactly the file update-project-versions will read, or it // would validate one file and apply another. const propsFile = releaserConfigFileName(trainVersion); @@ -140,6 +147,11 @@ jobs: } } + if (internal && !commercial) { + fail(`ERROR: ${trainVersion} is an -INTERNAL train, and -internal branches exist only ` + + 'in the -commercial repositories. Set commercial, or use -commercial project names.'); + } + // Where the config lives, from the shared module - it owns that choice for every // caller. `commercial` decides only which project repositories are updated. const releaseRepo = RELEASER_CONFIG_REPO; @@ -161,9 +173,9 @@ jobs: } console.log(`Found ${entries.length} version entries.`); - // spring-boot is in the properties file so that every project picks up its version, but - // it is not a Spring Cloud repository and nothing is pushed to it. - const NON_REPO_KEYS = new Set(['spring-boot']); + // spring-boot and spring-vault are in the properties file so that every project picks up + // their versions, but they are not Spring Cloud repositories and nothing is pushed to them. + const NON_REPO_KEYS = new Set(['spring-boot', 'spring-vault']); let wanted = null; if (names.length) { @@ -231,6 +243,7 @@ jobs: console.log(''); console.log(`commercial: ${commercial}`); + console.log(`internal: ${internal}`); console.log(`release repo: ${releaseRepo}`); console.log(`properties file: ${propsFile}`); console.log(''); @@ -243,6 +256,7 @@ jobs: fs.appendFileSync(out, `commercial=${commercial}\n`); fs.appendFileSync(out, `release-repo=${releaseRepo}\n`); fs.appendFileSync(out, `props-file=${propsFile}\n`); + fs.appendFileSync(out, `internal=${internal}\n`); JSEOF update: @@ -265,9 +279,9 @@ jobs: # ── resolve the branch to work on ──────────────────────────────────────────────── # The branch is always derived from the version in the properties file - there is no # input to override it. Between the .x derivation and the main fallback the action - # makes, every branch this workflow targets is covered: the only names in projects.json - # that are not ..x are main, and the -internal branches, which are rebased - # from the OSS branches this updates and so are never targeted here. + # makes, every OSS and commercial release-line branch this workflow targets is covered. + # An -INTERNAL train is the exception: it resolves to ..x-internal, with + # no fallback, and only in the commercial repos. - name: Resolve target branch id: branch uses: ./.github/actions/resolve-release-branch @@ -275,6 +289,7 @@ jobs: repo: ${{ matrix.repo }} version: ${{ matrix.version }} commercial: ${{ needs.setup.outputs.commercial }} + internal: ${{ needs.setup.outputs.internal }} token: ${{ inputs.token || secrets.GH_ACTIONS_REPO_TOKEN }} - name: Clone project