Skip to content

Commit 279d5ff

Browse files
committed
docs: add project README and GitHub Actions release workflow
- Create `README.md` with detailed usage, configuration, and security guidelines for the Spring Cloud Proxy library. - Add `release.yml` GitHub Actions workflow for automating version releases.
1 parent a1ffc16 commit 279d5ff

2 files changed

Lines changed: 358 additions & 0 deletions

File tree

.github/workflows/release.yml

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
workflow_dispatch:
8+
inputs:
9+
gradle_args:
10+
description: "Extra Gradle args for the release run, for example: --stacktrace"
11+
required: false
12+
default: ""
13+
type: string
14+
gradle_log_level:
15+
description: "Gradle log level used by workflow Gradle invocations"
16+
required: false
17+
default: info
18+
type: choice
19+
options:
20+
- quiet
21+
- warn
22+
- lifecycle
23+
- info
24+
- debug
25+
skip_verification:
26+
description: "Skip the verification build, then run the release directly"
27+
required: false
28+
default: false
29+
type: boolean
30+
run_release_task_tests:
31+
description: "Allow tests to run inside the Gradle release task"
32+
required: false
33+
default: false
34+
type: boolean
35+
36+
permissions:
37+
contents: write
38+
packages: write
39+
40+
concurrency:
41+
group: release-master
42+
cancel-in-progress: false
43+
44+
jobs:
45+
release:
46+
if: >-
47+
github.event_name == 'workflow_dispatch' ||
48+
(
49+
!contains(github.event.head_commit.message, '[Gradle Release Plugin]') &&
50+
!contains(github.event.head_commit.message, '[skip ci]') &&
51+
!contains(github.event.head_commit.message, '[ci skip]') &&
52+
!contains(github.event.head_commit.message, '[no ci]') &&
53+
!contains(github.event.head_commit.message, '[skip actions]') &&
54+
!contains(github.event.head_commit.message, '[actions skip]') &&
55+
!contains(github.event.head_commit.message, 'skip-checks:true')
56+
)
57+
runs-on: ubuntu-latest
58+
timeout-minutes: 120
59+
60+
env:
61+
GRADLE_OPTS: >-
62+
-Dorg.gradle.daemon=false
63+
-Dorg.gradle.internal.http.connectionTimeout=120000
64+
-Dorg.gradle.internal.http.socketTimeout=300000
65+
EXTRA_GRADLE_ARGS: ${{ github.event.inputs.gradle_args || '' }}
66+
GRADLE_LOG_LEVEL: ${{ github.event.inputs.gradle_log_level || 'info' }}
67+
SKIP_VERIFICATION: ${{ github.event.inputs.skip_verification || 'false' }}
68+
RUN_RELEASE_TASK_TESTS: ${{ github.event_name != 'workflow_dispatch' || inputs.run_release_task_tests }}
69+
TESTCONTAINERS_RYUK_DISABLED: "true"
70+
71+
steps:
72+
- name: Checkout
73+
uses: actions/checkout@v4
74+
with:
75+
fetch-depth: 0
76+
token: ${{ secrets.RELEASE_GITHUB_TOKEN || github.token }}
77+
78+
- name: Set up JDK 17
79+
uses: actions/setup-java@v4
80+
with:
81+
distribution: temurin
82+
java-version: "17"
83+
84+
- name: Set up Gradle
85+
uses: gradle/actions/setup-gradle@v4
86+
with:
87+
cache-read-only: false
88+
89+
- name: Configure git
90+
run: |
91+
git config user.name "github-actions[bot]"
92+
git config user.email "github-actions[bot]@users.noreply.github.com"
93+
94+
- name: Verify Docker environment
95+
if: env.SKIP_VERIFICATION != 'true'
96+
run: |
97+
docker version
98+
docker info
99+
100+
- name: Run verification build
101+
if: env.SKIP_VERIFICATION != 'true'
102+
run: |
103+
./gradlew --no-configuration-cache build --stacktrace "--$GRADLE_LOG_LEVEL"
104+
105+
- name: Collect diagnostics on failure
106+
if: failure() && env.SKIP_VERIFICATION != 'true'
107+
run: |
108+
set +e
109+
diagnostics_dir="build/github-diagnostics"
110+
mkdir -p "${diagnostics_dir}/docker"
111+
112+
docker ps -a | tee "${diagnostics_dir}/docker/containers.txt"
113+
docker images | tee "${diagnostics_dir}/docker/images.txt"
114+
115+
mapfile -t container_ids < <(docker ps -aq)
116+
for container_id in "${container_ids[@]}"; do
117+
container_name="$(docker inspect --format '{{.Name}}' "${container_id}" 2>/dev/null | sed 's#^/##')"
118+
if [ -z "${container_name}" ]; then
119+
container_name="${container_id}"
120+
fi
121+
safe_name="$(printf '%s' "${container_name}" | tr -c 'A-Za-z0-9_.-' '_')"
122+
docker inspect "${container_id}" > "${diagnostics_dir}/docker/${safe_name}.inspect.json" 2>&1
123+
docker logs "${container_id}" > "${diagnostics_dir}/docker/${safe_name}.stdout-stderr.log" 2>&1
124+
done
125+
126+
if find . -path '*/build/test-results/*' -type f -name '*.xml' -print -quit | grep -q .; then
127+
while IFS= read -r result_file; do
128+
echo "--- ${result_file} ---"
129+
sed -n '1,240p' "${result_file}"
130+
done < <(find . -path '*/build/test-results/*' -type f -name '*.xml' -print | sort)
131+
fi
132+
133+
- name: Upload test reports and container logs
134+
if: failure() && env.SKIP_VERIFICATION != 'true'
135+
uses: actions/upload-artifact@v4
136+
with:
137+
name: test-diagnostics-${{ github.run_id }}-${{ github.run_attempt }}
138+
path: |
139+
**/build/reports/tests/
140+
**/build/test-results/
141+
**/build/reports/problems/
142+
build/github-diagnostics/
143+
if-no-files-found: ignore
144+
retention-days: 14
145+
146+
- name: Write signing key
147+
env:
148+
SIGNING_KEY_ASC: ${{ secrets.SIGNING_KEY_ASC }}
149+
run: |
150+
printf '%s' "$SIGNING_KEY_ASC" > "$RUNNER_TEMP/signing-key.asc"
151+
152+
- name: Capture release metadata
153+
run: |
154+
release_version="$(awk -F= '/^[[:space:]]*version[[:space:]]*=/{gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' gradle.properties)"
155+
if [ -z "$release_version" ]; then
156+
echo "Could not read version from gradle.properties" >&2
157+
exit 1
158+
fi
159+
release_version="${release_version%-SNAPSHOT}"
160+
echo "RELEASE_VERSION=${release_version}" >> "$GITHUB_ENV"
161+
162+
- name: Run release
163+
id: run_release
164+
timeout-minutes: 90
165+
env:
166+
OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
167+
OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
168+
SIGNING_KEY_FILE: ${{ runner.temp }}/signing-key.asc
169+
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
170+
run: |
171+
release_args=()
172+
if [ "$SKIP_VERIFICATION" = "true" ] || [ "$RUN_RELEASE_TASK_TESTS" != "true" ]; then
173+
release_args=(-x test)
174+
fi
175+
./gradlew --no-configuration-cache release --stacktrace "--$GRADLE_LOG_LEVEL" "${release_args[@]}" $EXTRA_GRADLE_ARGS
176+
177+
- name: Write release publish summary
178+
if: always() && env.RELEASE_VERSION != ''
179+
env:
180+
RELEASE_OUTCOME: ${{ steps.run_release.outcome }}
181+
run: |
182+
group_id="org.openprojectx.spring.cloud.proxy"
183+
version="${RELEASE_VERSION}"
184+
artifacts=(
185+
"app"
186+
"core"
187+
"spring-cloud-proxy-spring-boot-autoconfigure"
188+
"spring-cloud-proxy-spring-boot-starter"
189+
)
190+
191+
{
192+
if [ "$RELEASE_OUTCOME" = "success" ]; then
193+
echo "## Release publish summary"
194+
else
195+
echo "## Release publish attempt"
196+
fi
197+
echo
198+
echo "- Version: \`${version}\`"
199+
echo "- Release result: \`${RELEASE_OUTCOME}\`"
200+
echo "- Maven group: \`${group_id}\`"
201+
echo
202+
if [ "$RELEASE_OUTCOME" != "success" ]; then
203+
echo "> The release step did not complete successfully. The artifacts below are planned coordinates; inspect the Gradle log for partial uploads."
204+
echo
205+
fi
206+
echo "| Artifact | Maven coordinate | Maven Central |"
207+
echo "|---|---|---|"
208+
for artifact in "${artifacts[@]}"; do
209+
coordinate="${group_id}:${artifact}:${version}"
210+
central_url="https://central.sonatype.com/artifact/${group_id}/${artifact}/${version}"
211+
echo "| \`${artifact}\` | \`${coordinate}\` | [artifact](${central_url}) |"
212+
done
213+
echo
214+
echo "> Maven Central search can lag briefly after Sonatype release completes."
215+
} >> "$GITHUB_STEP_SUMMARY"

README.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Spring Cloud Proxy
2+
3+
Extensible proxy utilities built on Spring Cloud Gateway. The first utility is a dynamic HTTP proxy that selects the downstream scheme, host, port, and path from the incoming URL.
4+
5+
The project targets Java 17, Spring Boot 4, and Spring Cloud 2025.1.1.
6+
7+
## Installation
8+
9+
Add the starter to a Spring Boot application.
10+
11+
### Gradle
12+
13+
```kotlin
14+
dependencies {
15+
implementation("org.openprojectx.spring.cloud.proxy:spring-cloud-proxy-spring-boot-starter:0.1.0")
16+
}
17+
```
18+
19+
### Maven
20+
21+
```xml
22+
<dependency>
23+
<groupId>org.openprojectx.spring.cloud.proxy</groupId>
24+
<artifactId>spring-cloud-proxy-spring-boot-starter</artifactId>
25+
<version>0.1.0</version>
26+
</dependency>
27+
```
28+
29+
The starter auto-configures Spring Cloud Gateway and the dynamic proxy route.
30+
31+
## Dynamic proxy
32+
33+
Requests use this format:
34+
35+
```text
36+
http://proxy-server/dynamic-proxy/{scheme}/{host}/{port}/{target-path}
37+
```
38+
39+
For example:
40+
41+
```text
42+
POST http://localhost:8080/dynamic-proxy/https/api.example.com/443/v1/orders?active=true
43+
```
44+
45+
The request method, body, permitted headers, query parameters, and encoded target path are forwarded. The target's status, headers, and body are returned to the caller.
46+
47+
Only `http` and `https` schemes are accepted by default. Invalid schemes, ports, and route formats return `400 Bad Request`.
48+
49+
## Request header isolation
50+
51+
Proxy credentials and cookies should not normally be exposed to the target. By default, the dynamic proxy removes:
52+
53+
- `Authorization`
54+
- `Cookie`
55+
56+
A target-specific authorization value can coexist with the proxy's authorization header:
57+
58+
```http
59+
Authorization: Bearer proxy-credential
60+
X-Target-Authorization: Bearer target-credential
61+
```
62+
63+
The target receives only:
64+
65+
```http
66+
Authorization: Bearer target-credential
67+
```
68+
69+
`X-Target-Authorization` is removed after it is mapped.
70+
71+
The removal and mapping policies are configurable:
72+
73+
```yaml
74+
spring:
75+
cloud:
76+
proxy:
77+
dynamic:
78+
removed-request-headers:
79+
- Authorization
80+
- Cookie
81+
- X-Internal-Token
82+
request-header-mappings:
83+
- source: X-Target-Authorization
84+
target: Authorization
85+
- source: X-Target-Api-Key
86+
target: X-Api-Key
87+
```
88+
89+
Mapped values are captured before removal, so a mapping source may also be listed in `removed-request-headers`. A mapping source header is never sent downstream under its original name.
90+
91+
## Configuration
92+
93+
| Property | Default | Description |
94+
|---|---|---|
95+
| `spring.cloud.proxy.dynamic.enabled` | `true` | Enables the dynamic proxy route. |
96+
| `spring.cloud.proxy.dynamic.path-prefix` | `/dynamic-proxy` | Changes the public route prefix. |
97+
| `spring.cloud.proxy.dynamic.allowed-schemes` | `http`, `https` | Schemes callers may select. |
98+
| `spring.cloud.proxy.dynamic.removed-request-headers` | `Authorization`, `Cookie` | Request headers removed before routing. |
99+
| `spring.cloud.proxy.dynamic.request-header-mappings` | `X-Target-Authorization` → `Authorization` | Source-to-target header mappings. |
100+
101+
Setting a collection property replaces its default collection. Repeat any defaults you want to retain.
102+
103+
## Run locally
104+
105+
```bash
106+
./gradlew :app:bootRun
107+
```
108+
109+
Example request:
110+
111+
```bash
112+
curl --request POST \
113+
'http://localhost:8080/dynamic-proxy/https/httpbin.org/443/anything?source=proxy' \
114+
--header 'Authorization: Bearer proxy-credential' \
115+
--header 'X-Target-Authorization: Bearer target-credential' \
116+
--header 'Content-Type: application/json' \
117+
--data '{"hello":"gateway"}'
118+
```
119+
120+
## Security
121+
122+
This utility intentionally lets a caller select a network destination, which can create server-side request forgery risk. Protect the proxy with authentication and authorization, expose it only to trusted clients, and restrict its network access to destinations callers are permitted to reach.
123+
124+
## Modules
125+
126+
| Module | Purpose |
127+
|---|---|
128+
| `core` | Shared foundation for future proxy utilities. |
129+
| `spring-cloud-proxy-spring-boot-autoconfigure` | Dynamic proxy implementation and Spring Boot auto-configuration. |
130+
| `spring-cloud-proxy-spring-boot-starter` | Consumer-facing starter and dependency management. |
131+
| `app` | Runnable example application. |
132+
133+
## Tests
134+
135+
Docker is required for the integration test, which uses a generic Testcontainers container running `wiremock/wiremock:3.13.2`.
136+
137+
```bash
138+
./gradlew build --no-configuration-cache
139+
```
140+
141+
## License
142+
143+
Licensed under the Apache License 2.0. See [LICENSE](LICENSE).

0 commit comments

Comments
 (0)