Compare commits
2
Commits
7a6139017f
...
d044277194
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d044277194 | ||
|
|
d13443e338 |
@@ -1,8 +1,10 @@
|
||||
name: Container
|
||||
|
||||
'on':
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -12,17 +14,66 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
TESTCONTAINERS_HOST_OVERRIDE: host.docker.internal
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Java 25
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '25'
|
||||
cache: maven
|
||||
|
||||
- name: Set up Node 24
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: npm
|
||||
|
||||
- name: Verify Docker for PostgreSQL tests
|
||||
run: docker info
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run frontend tests
|
||||
run: npm run test:ui
|
||||
|
||||
- name: Build frontend assets
|
||||
run: npm run build
|
||||
|
||||
- name: Verify generated assets are committed
|
||||
run: git diff --exit-code -- src/main/resources/static/assets/app.css src/main/resources/static/assets/icons.svg
|
||||
|
||||
- name: Run Maven tests
|
||||
run: ./mvnw -B test
|
||||
|
||||
- name: Verify Javadoc
|
||||
run: ./mvnw -B -DskipTests -Ddoclint=all javadoc:javadoc
|
||||
|
||||
- name: Verify whitespace
|
||||
run: git diff --check
|
||||
|
||||
amd64:
|
||||
needs: verify
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
|
||||
- name: Select image tags
|
||||
id: image
|
||||
@@ -54,14 +105,14 @@ jobs:
|
||||
|
||||
- name: Log in to registry
|
||||
if: steps.image.outputs.publish == 'true'
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: ${{ steps.image.outputs.registry }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Build AMD64 image and publish main
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
@@ -73,42 +124,52 @@ jobs:
|
||||
arm64:
|
||||
# Gitea cannot schedule a probe on a missing label. Enable this repository variable only
|
||||
# while a trusted ubuntu-latest-arm runner is registered and online.
|
||||
if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main' && vars.ARM64_RUNNER_AVAILABLE == 'true'
|
||||
if: vars.ARM64_RUNNER_AVAILABLE == 'true'
|
||||
needs: verify
|
||||
runs-on: ubuntu-latest-arm
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
|
||||
- name: Require image configuration
|
||||
- name: Select image tag
|
||||
id: image
|
||||
env:
|
||||
CONFIGURED_IMAGE: ${{ vars.CONTAINER_IMAGE }}
|
||||
EVENT_NAME: ${{ gitea.event_name }}
|
||||
GITEA_REF: ${{ gitea.ref }}
|
||||
GITEA_SHA: ${{ gitea.sha }}
|
||||
run: |
|
||||
publish=false
|
||||
image=labtimesheet
|
||||
if [ "$EVENT_NAME" = "push" ] && [ "$GITEA_REF" = "refs/heads/main" ]; then
|
||||
test -n "$CONFIGURED_IMAGE" || { echo "Repository variable CONTAINER_IMAGE is required" >&2; exit 1; }
|
||||
echo "image=$CONFIGURED_IMAGE" >> "$GITHUB_OUTPUT"
|
||||
echo "registry=${CONFIGURED_IMAGE%%/*}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=$CONFIGURED_IMAGE:sha-${GITEA_SHA}-arm64" >> "$GITHUB_OUTPUT"
|
||||
publish=true
|
||||
image="$CONFIGURED_IMAGE"
|
||||
fi
|
||||
echo "publish=$publish" >> "$GITHUB_OUTPUT"
|
||||
echo "registry=${image%%/*}" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=$image:sha-${GITEA_SHA}-arm64" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Log in to registry
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
if: steps.image.outputs.publish == 'true'
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: ${{ steps.image.outputs.registry }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
|
||||
- name: Build native ARM64 image
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/arm64
|
||||
push: true
|
||||
push: ${{ steps.image.outputs.publish }}
|
||||
tags: ${{ steps.image.outputs.tag }}
|
||||
build-args: |
|
||||
VCS_REF=${{ gitea.sha }}
|
||||
@@ -120,7 +181,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Set up Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
|
||||
- name: Resolve registry
|
||||
id: image
|
||||
@@ -131,7 +192,7 @@ jobs:
|
||||
echo "registry=${CONFIGURED_IMAGE%%/*}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Log in to registry
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: ${{ steps.image.outputs.registry }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
|
||||
@@ -15,21 +15,23 @@ jobs:
|
||||
verify:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
TESTCONTAINERS_HOST_OVERRIDE: host.docker.internal
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Java 25
|
||||
uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4
|
||||
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '25'
|
||||
cache: maven
|
||||
|
||||
- name: Set up Node 24
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: npm
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ Configure these repository settings:
|
||||
| Secret | `REGISTRY_USERNAME` | Registry user allowed to publish this package |
|
||||
| Secret | `REGISTRY_TOKEN` | Registry token with package write access |
|
||||
|
||||
`verify.yml` runs for every pull request and push. `container.yml` builds AMD64 for every pull request and push, but publishes only a push to `main`. Main always receives immutable `sha-<commit>` and convenience `main` tags.
|
||||
`verify.yml` runs for every pull request and push. `container.yml` runs only when manually dispatched or when `main` is pushed, and it repeats verification before either architecture build. Manual runs build without publishing. A push to `main` publishes immutable `sha-<commit>` and convenience `main` tags.
|
||||
|
||||
When ARM64 is disabled, those canonical tags remain valid AMD64 images and the workflow succeeds. When it is enabled, the native ARM runner publishes an architecture tag and the final job replaces the canonical tags with a combined AMD64/ARM64 manifest. Gitea cannot discover an unavailable runner from inside an unscheduled job, so the repository variable is the deliberate availability gate.
|
||||
|
||||
|
||||
@@ -118,11 +118,12 @@ companion record under [`docs/tests`](docs/tests/README.md).
|
||||
|
||||
## Continuous integration and production containers
|
||||
|
||||
Gitea Actions now verifies every pull request and push. A separate container
|
||||
workflow builds Linux AMD64 and publishes only from `main`; native Linux ARM64
|
||||
is added only when the repository explicitly declares that its ARM runner is
|
||||
online. Every published revision has an immutable `sha-<full-commit>` tag, with
|
||||
`main` as a convenience alias.
|
||||
Gitea Actions verifies every pull request and push. The separate container
|
||||
workflow runs only for a manual dispatch or a push to `main`, and its verification
|
||||
job must pass before either image build starts. Manual runs build without publishing;
|
||||
`main` pushes publish Linux AMD64 and add native Linux ARM64 only when the repository
|
||||
explicitly declares that its ARM runner is online. Every published revision has an
|
||||
immutable `sha-<full-commit>` tag, with `main` as a convenience alias.
|
||||
|
||||
The production image is a non-root Java 25 image. The root [compose.yaml](compose.yaml)
|
||||
supports either a persistent PostgreSQL 18.4 sidecar or an external PostgreSQL
|
||||
|
||||
+7
-2
@@ -223,8 +223,9 @@ claim never replaces a test command and result.
|
||||
|
||||
Gitea runs the frontend tests/build, complete Maven/PostgreSQL suite, Javadoc,
|
||||
generated-asset check, and whitespace check for every pull request and push.
|
||||
The separate container workflow builds the production Dockerfile without
|
||||
publishing pull-request or work-branch images. Only `main` publishes.
|
||||
The separate container workflow runs only when manually dispatched or when
|
||||
`main` is pushed. It repeats the verification job before building either image.
|
||||
Manual runs do not publish; only a push to `main` publishes.
|
||||
|
||||
Run focused and affected tests locally before pushing. CI is the shared
|
||||
confirmation, not a substitute for local RED and GREEN evidence.
|
||||
@@ -236,6 +237,10 @@ confirmation, not a substitute for local RED and GREEN evidence.
|
||||
Start Docker Desktop or OrbStack. Run `docker version`. OrbStack users should
|
||||
also check the `DOCKER_HOST` command shown in Section 1.
|
||||
|
||||
The Gitea Docker runner exposes the daemon through Docker Desktop, so its jobs
|
||||
set `TESTCONTAINERS_HOST_OVERRIDE=host.docker.internal`. Keep that override if
|
||||
the runner stays containerized; otherwise Ryuk may try an unreachable bridge IP.
|
||||
|
||||
### The wrong Java version is used
|
||||
|
||||
Run `java -version` and `./mvnw -version`. Both should report Java 25. Set
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# Test Evidence: Gitea Testcontainers and container workflow gates
|
||||
|
||||
- **Test type:** Integration
|
||||
- **Requirement IDs:** `OPS-011`, `OPS-012`, `TST-001`, `TST-005`, `TST-009`
|
||||
- **Scenario IDs:** `AC-OPS-002`, `AC-OPS-004`
|
||||
- **Test class/method:** `src/test/js/delivery-contract.test.mjs`
|
||||
- **Implementation commit:** `d13443e338770dec0ca9822600f9a9d8405dfdbb`
|
||||
|
||||
## Protected behavior
|
||||
|
||||
Gitea verification must reach Docker Desktop-published Testcontainers ports from
|
||||
inside its job container. Container builds may start only after an equivalent
|
||||
verification job succeeds, and the container workflow may run only by manual
|
||||
dispatch or by a push to `main`. Every third-party workflow action is pinned to
|
||||
the reviewed latest release commit rather than a moving tag.
|
||||
|
||||
## Test method
|
||||
|
||||
The dependency-free delivery contract reads both workflow files and checks the
|
||||
Testcontainers host override, event filters, verify-to-build dependencies, and
|
||||
the complete allowlist of immutable action SHAs. The remote failure log supplies
|
||||
the production-shaped network reproduction because it ran inside the real Gitea
|
||||
Docker runner.
|
||||
|
||||
## Hand-derived expected result
|
||||
|
||||
The runner already resolves `host.docker.internal` to its Docker host. Therefore
|
||||
Testcontainers must use that host instead of the job-network gateway
|
||||
`172.17.0.1`. Pull requests and non-main branch pushes must never schedule the
|
||||
container workflow. Manual dispatches build but do not publish, while main pushes
|
||||
publish only after verification succeeds.
|
||||
|
||||
## RED
|
||||
|
||||
**Command**
|
||||
|
||||
```text
|
||||
env PATH=/opt/homebrew/opt/node@24/bin:/usr/bin:/bin \
|
||||
node --test src/test/js/delivery-contract.test.mjs
|
||||
|
||||
tea actions runs logs 174 --repo sechmachine/labtimesheet \
|
||||
--login sechmachine-git
|
||||
```
|
||||
|
||||
**Observed result**
|
||||
|
||||
```text
|
||||
Delivery contract: 4 tests, 1 passed, 3 failed. The workflows lacked the
|
||||
Testcontainers host override, container event/dependency gates, and current
|
||||
action pins.
|
||||
|
||||
Gitea run 174 found Docker at unix:///var/run/docker.sock but selected host
|
||||
172.17.0.1. Ryuk started, then repeated connections to 172.17.0.1:57499 were
|
||||
refused. Maven ended with 217 tests, 64 errors.
|
||||
```
|
||||
|
||||
## GREEN
|
||||
|
||||
**Command**
|
||||
|
||||
```text
|
||||
env PATH=/opt/homebrew/opt/node@24/bin:/usr/bin:/bin \
|
||||
node --test src/test/js/delivery-contract.test.mjs
|
||||
```
|
||||
|
||||
**Observed result**
|
||||
|
||||
```text
|
||||
Delivery contract: 4 tests, 4 passed.
|
||||
```
|
||||
|
||||
## Affected suite
|
||||
|
||||
**Command and result**
|
||||
|
||||
```text
|
||||
env PATH=/opt/homebrew/opt/node@24/bin:/usr/bin:/bin npm run test:ui
|
||||
Result: 5 tests passed.
|
||||
|
||||
env JAVA_HOME=/opt/homebrew/opt/openjdk@25 \
|
||||
PATH=/opt/homebrew/opt/openjdk@25/bin:/opt/homebrew/bin:/usr/bin:/bin \
|
||||
DOCKER_HOST=unix:///Users/sechmachine/.orbstack/run/docker.sock \
|
||||
./mvnw -B test
|
||||
Result: 205 tests passed across 44 suites; 0 failures, errors, or skips.
|
||||
|
||||
npm ci && npm run build
|
||||
Result: Tailwind and Lucide assets built successfully; tracked assets remained unchanged.
|
||||
|
||||
./mvnw -B -DskipTests -Ddoclint=all javadoc:javadoc
|
||||
Result: BUILD SUCCESS with 83 existing missing-comment warnings and no production Java change.
|
||||
|
||||
Ruby YAML parsing and git diff --check
|
||||
Result: both workflow files parsed and the diff check passed.
|
||||
|
||||
Gitea Actions run 177 on `work/fix/platform/ci-testcontainers-actions`
|
||||
Result: Verify completed successfully in 8 minutes on the real Docker-mode runner.
|
||||
The non-main branch push scheduled `verify.yml` only; `container.yml` did not run.
|
||||
```
|
||||
|
||||
## External-test boundaries
|
||||
|
||||
The local contract cannot prove action-runner compatibility, registry credentials,
|
||||
or availability of the optional ARM runner. Those are checked by the actual Gitea
|
||||
branch verification and main container runs. Release freshness was checked against
|
||||
the official upstream release APIs on 2026-08-15; the immutable pins remain stable,
|
||||
but a later release requires an intentional reviewed update.
|
||||
@@ -6,6 +6,14 @@ import test from "node:test";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
const read = (path) => readFileSync(resolve(root, path), "utf8");
|
||||
const expectedActionPins = new Map([
|
||||
["actions/checkout", "3d3c42e5aac5ba805825da76410c181273ba90b1"],
|
||||
["actions/setup-java", "b6effb05e454b25005698d916606bdc6ffcbf961"],
|
||||
["actions/setup-node", "820762786026740c76f36085b0efc47a31fe5020"],
|
||||
["docker/setup-buildx-action", "bb05f3f5519dd87d3ba754cc423b652a5edd6d2c"],
|
||||
["docker/login-action", "dbcb813823bdd20940b903addbd779551569679f"],
|
||||
["docker/build-push-action", "53b7df96c91f9c12dcc8a07bcb9ccacbed38856a"],
|
||||
]);
|
||||
|
||||
test("production image and Compose keep one image usable with bundled or external PostgreSQL", () => {
|
||||
const dockerfile = read("Dockerfile");
|
||||
@@ -37,12 +45,19 @@ test("verification workflow checks every pull request and pushed branch without
|
||||
assert.match(workflow, /npm run test:ui/);
|
||||
assert.match(workflow, /npm run build/);
|
||||
assert.match(workflow, /\.\/mvnw -B test/);
|
||||
assert.match(workflow, /TESTCONTAINERS_HOST_OVERRIDE: host\.docker\.internal/);
|
||||
assert.doesNotMatch(workflow, /permissions:\s*write-all/);
|
||||
});
|
||||
|
||||
test("container workflow publishes only main and makes native ARM64 explicitly optional", () => {
|
||||
test("container workflow runs only manually or on main and verifies before either image build", () => {
|
||||
const workflow = read(".gitea/workflows/container.yml");
|
||||
|
||||
assert.match(workflow, /'on':\n workflow_dispatch:\n push:\n branches:\n - main/);
|
||||
assert.doesNotMatch(workflow, /^ pull_request:/m);
|
||||
assert.match(workflow, /jobs:\n verify:/);
|
||||
assert.match(workflow, /amd64:\n needs: verify/);
|
||||
assert.match(workflow, /arm64:[\s\S]*?needs: verify/);
|
||||
assert.match(workflow, /TESTCONTAINERS_HOST_OVERRIDE: host\.docker\.internal/);
|
||||
assert.match(workflow, /runs-on: ubuntu-latest-arm/);
|
||||
assert.match(workflow, /vars\.ARM64_RUNNER_AVAILABLE == 'true'/);
|
||||
assert.match(workflow, /gitea\.ref == 'refs\/heads\/main'/);
|
||||
@@ -51,3 +66,18 @@ test("container workflow publishes only main and makes native ARM64 explicitly o
|
||||
assert.match(workflow, /imagetools create/);
|
||||
assert.doesNotMatch(workflow, /ssh|DEPLOY_HOST|DEPLOY_KEY/i);
|
||||
});
|
||||
|
||||
test("workflows pin every action to the latest reviewed immutable release", () => {
|
||||
const workflows = [
|
||||
read(".gitea/workflows/verify.yml"),
|
||||
read(".gitea/workflows/container.yml"),
|
||||
].join("\n");
|
||||
const uses = [...workflows.matchAll(/uses:\s+([^@\s]+)@([0-9a-f]{40})/g)];
|
||||
|
||||
assert.ok(uses.length > 0);
|
||||
for (const [, action, pin] of uses) {
|
||||
assert.equal(pin, expectedActionPins.get(action), `unexpected pin for ${action}`);
|
||||
}
|
||||
assert.deepEqual(new Set(uses.map(([, action]) => action)), new Set(expectedActionPins.keys()));
|
||||
assert.doesNotMatch(workflows, /uses:\s+[^\s]+@v\d/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user