Skip to content

Commit 98ed5f6

Browse files
committed
ci(release): publish after manual dispatch without environment approval
1 parent 393e457 commit 98ed5f6

6 files changed

Lines changed: 56 additions & 45 deletions

File tree

‎.github/scripts/central_release.py‎

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -110,20 +110,6 @@ def check_ci(info):
110110
'Quality and all five Java CI jobs must succeed')
111111

112112

113-
def check_environment():
114-
environment = gh('environments/release')
115-
require(environment and any(rule.get('type') == 'required_reviewers' and rule.get('reviewers')
116-
for rule in environment.get('protection_rules', [])),
117-
'Configure required reviewers on the release environment before publication')
118-
policy = environment.get('deployment_branch_policy') or {}
119-
require(policy.get('custom_branch_policies') and not policy.get('protected_branches'),
120-
'Configure the release environment with a main-only branch policy')
121-
branches = gh('environments/release/deployment-branch-policies')
122-
entries = branches.get('branch_policies', []) if branches else []
123-
require(len(entries) == 1 and entries[0].get('name') == 'main' and entries[0].get('type') == 'branch',
124-
'release environment must allow only the main branch')
125-
126-
127113
def read_bundle(path, version):
128114
prefix = f'com/qoder/qca-sdk/{version}/qca-sdk-{version}'
129115
primary = {prefix + suffix for suffix in ('.pom', '.jar', '-sources.jar', '-javadoc.jar')}
@@ -162,7 +148,6 @@ def preflight(source, output):
162148
check_main(info)
163149
require(gh(f'git/ref/tags/{tag_name(info)}') is None, 'Tag already exists; use resume_run_id')
164150
check_ci(info)
165-
check_environment()
166151
output.mkdir(parents=True, exist_ok=True)
167152
save(output / 'intent.json', info)
168153
(output / 'release-notes.md').write_text(notes)

‎.github/scripts/test_central_release.py‎

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -125,17 +125,42 @@ def test_tag_rejects_moved_sha_and_changed_bundle(self):
125125
with patch.object(release, 'gh', side_effect=[ref, tag]), self.assertRaisesRegex(ValueError, 'source differs'):
126126
release.check_tag(self.info)
127127

128-
def test_release_requires_reviewers_and_main_only_environment(self):
129-
environment = dict(protection_rules=[dict(type='required_reviewers', reviewers=[dict(id=1)])],
130-
deployment_branch_policy=dict(custom_branch_policies=True, protected_branches=False))
131-
branches = dict(branch_policies=[dict(name='main', type='branch')])
132-
with patch.object(release, 'gh', side_effect=[environment, branches]):
133-
release.check_environment()
134-
with patch.object(release, 'gh', return_value=None), self.assertRaisesRegex(ValueError, 'required reviewers'):
135-
release.check_environment()
136-
branches['branch_policies'].append(dict(name='*', type='branch'))
137-
with patch.object(release, 'gh', side_effect=[environment, branches]), self.assertRaisesRegex(ValueError, 'only the main'):
138-
release.check_environment()
128+
def test_preflight_without_environment_still_requires_current_main_and_ci(self):
129+
source, candidate = self.output / 'source', self.output / 'candidate'
130+
coordinates = '<groupId>com.qoder</groupId><artifactId>qca-sdk</artifactId><version>1.2.3</version>'
131+
project = '<project xmlns="http://maven.apache.org/POM/4.0.0">{}</project>'
132+
files = {
133+
'pom.xml': project.format(coordinates),
134+
'examples/pom.xml': project.format(
135+
'<version>1.2.3</version><dependencies><dependency>' + coordinates + '</dependency></dependencies>'),
136+
'src/main/resources/com/qoder/cloudagents/version.properties': 'version=${project.version}\n',
137+
'CHANGELOG.md': '# Changelog\n\n## [Unreleased]\n\n## [1.2.3]\n\n### Added\n\n- Release example.\n',
138+
}
139+
for name, contents in files.items():
140+
path = source / name
141+
path.parent.mkdir(parents=True, exist_ok=True)
142+
path.write_text(contents)
143+
responses = {
144+
'commits/main': dict(sha='a' * 40),
145+
'git/ref/tags/v1.2.3': None,
146+
'actions/workflows/ci.yml/runs?event=push&branch=main&head_sha=' + 'a' * 40:
147+
dict(workflow_runs=[dict(id=10, head_sha='a' * 40, conclusion='success')]),
148+
'actions/runs/10/jobs?per_page=100':
149+
dict(jobs=[dict(name=name, conclusion='success') for name in release.CHECKS]),
150+
}
151+
# Any request for repository protections or an environment fails this test.
152+
with patch.object(release, 'git', return_value='a' * 40), \
153+
patch.object(release, 'gh', side_effect=responses.__getitem__):
154+
release.preflight(source, candidate)
155+
self.assertEqual(release.identity(), json.loads((candidate / 'intent.json').read_text()))
156+
self.assertIn('Release example.', (candidate / 'release-notes.md').read_text())
157+
responses['commits/main']['sha'] = 'c' * 40
158+
with self.assertRaisesRegex(ValueError, 'main changed'):
159+
release.preflight(source, candidate)
160+
responses['commits/main']['sha'] = 'a' * 40
161+
responses['actions/runs/10/jobs?per_page=100']['jobs'][0]['conclusion'] = 'failure'
162+
with self.assertRaisesRegex(ValueError, 'all five'):
163+
release.preflight(source, candidate)
139164

140165
def test_recovery_cannot_upload(self):
141166
with patch.dict(os.environ, RESUME_RUN_ID='123'), patch.object(release, 'check_tag'), \

‎.github/workflows/release.yml‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ jobs:
7171
make quality
7272
make test
7373
cp target/package-check.json ../candidate/
74-
- name: Show source and release notes for review
74+
- name: Record source, release notes and candidate digests
7575
run: |
7676
printf '### Release source\n\nVersion: `%s`\n\nSHA: `%s`\n\nBatch: `%s`\n\n' "$RELEASE_VERSION" "$RELEASE_SHA" "$RELEASE_BATCH" >> "$GITHUB_STEP_SUMMARY"
7777
cat candidate/release-notes.md >> "$GITHUB_STEP_SUMMARY"
@@ -93,7 +93,6 @@ jobs:
9393
needs: preflight
9494
runs-on: ubuntu-latest
9595
timeout-minutes: 50
96-
environment: release
9796
permissions:
9897
contents: write
9998
actions: read
@@ -127,7 +126,7 @@ jobs:
127126
MAVEN_GPG_KEY: ${{ secrets.MAVEN_GPG_KEY }}
128127
MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
129128
run: |
130-
test "$CENTRAL_NAMESPACE" = com.qoder || { echo 'Verify com.qoder in Central Portal and set CENTRAL_NAMESPACE in the release environment'; exit 1; }
129+
test "$CENTRAL_NAMESPACE" = com.qoder || { echo 'Verify com.qoder in Central Portal and set the CENTRAL_NAMESPACE repository Actions variable'; exit 1; }
131130
test -n "$CENTRAL_USERNAME" && test -n "$CENTRAL_PASSWORD" || { echo 'Central Portal token secrets are missing'; exit 1; }
132131
if [[ -z "$RESUME_RUN_ID" ]]; then
133132
test -n "$MAVEN_GPG_KEY" && test -n "$MAVEN_GPG_PASSPHRASE" || { echo 'GPG signing secrets are missing'; exit 1; }

‎CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
### Validation
66

7-
- Manual Maven Central release workflow with source/CI checks, reviewed artifact digests, signing, retained deployment recovery, public package verification and matching GitHub Release notes. Publishing requires separate account and environment setup.
7+
- Manual Maven Central release workflow with source/CI checks, verified artifact digests, signing, retained deployment recovery, public package verification and matching GitHub Release notes. Publishing uses repository Actions secrets and proceeds after preflight without a separate environment approval.
88
- Disposable-key signing checks verify the release bundle without uploading it to Maven Central.
99
- Accept documented assistant-message projections in Realtime Work History while still checking Work IDs, terminal events and pagination.
1010
- Complete Schedule/Batch execution, Usage pagination/filtering, Identity SAT permissions/revocation and Realtime Work/History acceptance in CN and Global.

‎docs/maintaining.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,4 @@ The current version remains `0.1.0-SNAPSHOT`. A SNAPSHOT can resolve to differen
4343

4444
GitHub CI produces candidate JARs, sources, Javadoc, checksums and test reports. A successful CI run is not a Maven Central publication or a live-environment acceptance result. The default workflow uses mock services; run the opt-in checks in [live testing](live-testing.md) with separately supplied credentials and isolated resources when needed.
4545

46-
See [release setup](releasing.md) for repository protection, Central registration and signing secrets, the manual publication workflow and deployment recovery. The workflow requires a configured, reviewed `release` environment; adding workflow files alone does not enable public publication.
46+
See [release setup](releasing.md) for optional repository protection, Central registration and repository Actions secrets, the manual publication workflow and deployment recovery. A manual dispatch from `main` proceeds after preflight without a separate environment approval. Central credentials and signing setup are still required; adding workflow files alone does not enable public publication.

‎docs/releasing.md‎

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
# Java SDK release setup
22

3-
The development version is `com.qoder:qca-sdk:0.1.0-SNAPSHOT`. The manual [Release workflow](../.github/workflows/release.yml) implements publication, but cannot publish until the Central account, namespace, signing key and GitHub environment below are configured. The GitHub repository and internal Maven access do not grant Maven Central publishing access. No public release has been made by adding this workflow.
3+
The development version is `com.qoder:qca-sdk:0.1.0-SNAPSHOT`. The manual [Release workflow](../.github/workflows/release.yml) implements publication using repository Actions secrets. Dispatching it from `main` authorizes publication after all automated checks pass; there is no separate environment approval. The Central account, namespace, signing key and repository settings below must be configured first. The GitHub repository and internal Maven access do not grant Maven Central publishing access. No public release has been made by adding this workflow.
44

5-
## Activate GitHub protection
5+
## Optional GitHub protection
66

7-
After the first reviewed public source push, run CI and confirm all six checks described in [maintenance](maintaining.md) appear. A repository administrator can then apply the prepared payloads. Inspect existing rules first and update matching rules rather than creating duplicates:
7+
Branch/tag rules and a `release` environment are not prerequisites for this workflow. Its own checks require a manual dispatch from `main`, the intended source SHA and all six successful CI jobs described in [maintenance](maintaining.md).
8+
9+
A repository administrator can optionally apply the prepared main and tag protection payloads later. Inspect existing rules first and update matching rules rather than creating duplicates:
810

911
```sh
1012
gh api repos/QoderAI/qoder-cloud-agents-sdk-java/rulesets
1113
gh api --method POST repos/QoderAI/qoder-cloud-agents-sdk-java/rulesets --input .github/config/main-ruleset.json
1214
gh api --method POST repos/QoderAI/qoder-cloud-agents-sdk-java/rulesets --input .github/config/tag-ruleset.json
13-
gh api --method PUT repos/QoderAI/qoder-cloud-agents-sdk-java/environments/release --input .github/config/release-environment.json
14-
gh api --method POST repos/QoderAI/qoder-cloud-agents-sdk-java/environments/release/deployment-branch-policies --input .github/config/release-branch.json
1515
```
1616

17-
The main and `v*` tag protection follows the Go repository baseline. The prepared `release` environment uses maintainer `moonyue-w` (GitHub user ID 300878504), permits `main`, and does not prevent self-review, matching the existing SDK setup. Check that reviewer membership and repository policy are still correct before activation. Applying these settings requires repository administrator access. Configuration files do not activate remote settings by themselves.
17+
The main and `v*` tag protection follows the Go repository baseline. Configuration files do not activate remote settings by themselves. Without tag protection, the workflow still rejects conflicting tags and never overwrites them, but GitHub does not prevent another authorized actor from changing them.
18+
19+
The unused `release-environment.json` and `release-branch.json` files remain optional templates for adopting environment approval later. To use them, first review the named reviewer and policy, explicitly add `environment: release` to the publishing job, and configure its credentials in that environment. Creating an environment alone does not attach it to this workflow.
1820

1921
## Read-only release preflight
2022

@@ -48,7 +50,7 @@ The release workflow uses this bundle-only mode, then uploads the retained bundl
4850
4. In the publisher's [account page](https://central.sonatype.com/account), generate a **User Token**. The generated username/password pair is used for publication; it is not the normal account login password.
4951
5. Prepare a team-owned, passphrase-protected GPG signing key. Retain its backup securely and make its public key discoverable as required by [Central's GPG guide](https://central.sonatype.org/publish/requirements/gpg/). The workflow expects an ASCII-armored exported OpenPGP secret key, compatible with Maven GPG Plugin's BC signer. Export only the intended signing key.
5052

51-
In repository **Settings → Environments → release**, configure:
53+
In repository **Settings → Secrets and variables → Actions**, configure repository-level values:
5254

5355
| Kind | Name | Value |
5456
| --- | --- | --- |
@@ -58,15 +60,15 @@ In repository **Settings → Environments → release**, configure:
5860
| Secret | `MAVEN_GPG_KEY` | ASCII-armored exported private signing key |
5961
| Secret | `MAVEN_GPG_PASSPHRASE` | Passphrase for that signing key |
6062

61-
Do not put credentials in POMs, repository files, command-line arguments or chat. For example, `gh secret set CENTRAL_USERNAME --repo QoderAI/qoder-cloud-agents-sdk-java --env release` reads the value interactively; repeat for the password. An exported key file can be redirected to `gh secret set MAVEN_GPG_KEY ...` without printing it. The public signing key must be registered separately; configuring a private-key secret does not publish its public key.
63+
Do not put credentials in POMs, repository files, command-line arguments or chat. For example, `gh secret set CENTRAL_USERNAME --repo QoderAI/qoder-cloud-agents-sdk-java` reads the value interactively; repeat for the password and passphrase. An exported key file can be redirected to `gh secret set MAVEN_GPG_KEY ...` without printing it. Do not use `--env release`: environment-only secrets are unavailable to this workflow. The public signing key must be registered separately; configuring a private-key secret does not publish its public key.
6264

63-
The namespace variable is a maintainer's setup confirmation, not an automatic ownership check. Portal enforces actual publishing permissions during upload. The workflow stops before signing/uploading when its required settings are absent. Use the prepared required-reviewer and main-only environment rules above before the first release.
65+
The namespace variable is a maintainer's setup confirmation, not an automatic ownership check. Portal enforces actual publishing permissions during upload. The workflow stops before signing/uploading when its required settings are absent. Configuring repository secrets is separate from setting branch/tag protections; a repository administrator may need to enter the credentials if the current maintainer lacks that permission.
6466

6567
## Prepare and publish a release
6668

6769
Prepare a PR that updates the root POM, the example project's version and SDK dependency, plus the changelog version section. Keep an empty `Unreleased` section. Review any version-specific install examples. Merge only after `quality` and all five Java checks succeed. The workflow does not bump versions or merge the PR.
6870

69-
Open **Actions → Release → Run workflow**, select **main**, and enter:
71+
Open **Actions → Release → Run workflow**, select **main**, and verify the following inputs before dispatch. Successful preflight automatically proceeds to signing and publication; it does not pause for an additional approval.
7072

7173
| Input | First publication |
7274
| --- | --- |
@@ -78,12 +80,12 @@ Open **Actions → Release → Run workflow**, select **main**, and enter:
7880

7981
The stages are:
8082

81-
1. **preflight** checks input, POM/changelog, current main and the latest successful main CI run with all six required jobs. It re-runs quality, SDK tests and packaged consumer tests, then shows the notes and unsigned package hashes for approval.
82-
2. **publish** waits for the `release` environment reviewer. It rebuilds and signs with JDK 17, compares the JAR/POM/sources/Javadoc bytes to the reviewed candidate, and retains the exact signed bundle before any upload. It checks main again and creates an annotated `v<version>` tag binding the SHA, batch and bundle digest. It uploads once with `USER_MANAGED` publishing, immediately retains the deployment ID, waits for Central validation, requests publication, and waits for `PUBLISHED`.
83-
3. **verify** waits for the public Maven files and compares every payload, signature and checksum with the approved bundle. It then runs the independent example/consumer tests against Maven Central using empty Maven settings and a fresh local dependency cache; it never installs the SDK locally first.
83+
1. **preflight** checks input, POM/changelog, current main and the latest successful main CI run with all six required jobs. It re-runs quality, SDK tests and packaged consumer tests, then records the notes and unsigned package hashes in the run summary.
84+
2. **publish** starts automatically after preflight succeeds. It rebuilds and signs with JDK 17, compares the JAR/POM/sources/Javadoc bytes to the verified candidate, and retains the exact signed bundle before any upload. It checks main again and creates an annotated `v<version>` tag binding the SHA, batch and bundle digest. It uploads once with `USER_MANAGED` publishing, immediately retains the deployment ID, waits for Central validation, requests publication, and waits for `PUBLISHED`. `USER_MANAGED` separates Central validation from the publish API call; the workflow makes that call automatically.
85+
3. **verify** waits for the public Maven files and compares every payload, signature and checksum with the retained bundle. It then runs the independent example/consumer tests against Maven Central using empty Maven settings and a fresh local dependency cache; it never installs the SDK locally first.
8486
4. **release-notes** creates the matching GitHub Release only after verification succeeds. Existing notes must match exactly; the workflow does not overwrite conflicting releases.
8587

86-
The `release-bundle` artifact contains the signed archive, file digests, source/batch/workflow identity and approved notes. `central-receipt` records the deployment ID. Retention is 90 days; keep a durable copy for longer recovery needs. The workflow does not include signing secrets in artifacts. Public download verification or consumer failures do not undo a Central publication.
88+
The `release-bundle` artifact contains the signed archive, file digests, source/batch/workflow identity and release notes. `central-receipt` records the deployment ID. Retention is 90 days; keep a durable copy for longer recovery needs. The workflow does not include signing secrets in artifacts. Public download verification or consumer failures do not undo a Central publication.
8789

8890
## Recover without replacing a published version
8991

0 commit comments

Comments
 (0)