Compare commits

...
Author SHA1 Message Date
sechmachine 200a17e5bd docs(test): pin registry workflow evidence
Container / verify (push) Canceled after 4m34s
Container / amd64 (push) Canceled after 0s
Container / arm64 (push) Canceled after 0s
Container / manifest (push) Canceled after 0s
Verify / verify (push) Successful in 8m31s
2026-08-16 00:28:46 +07:00
sechmachine 4dd9f96a23 fix(ci): use Gitea actor for registry publishing 2026-08-16 00:28:15 +07:00
sechmachine d044277194 docs(test): record Gitea runner verification
Container / amd64 (push) Failing after 6m24s
Container / manifest (push) Skipped
Container / verify (push) Successful in 3m12s
Container / arm64 (push) Skipped
Verify / verify (push) Successful in 8m59s
2026-08-15 23:55:52 +07:00
sechmachine d13443e338 fix(ci): gate images behind verified Testcontainers
Verify / verify (push) Successful in 8m52s
2026-08-15 23:21:23 +07:00
8 changed files with 323 additions and 55 deletions
+98 -40
View File
@@ -1,8 +1,10 @@
name: Container name: Container
'on': 'on':
pull_request: workflow_dispatch:
push: push:
branches:
- main
permissions: permissions:
contents: read contents: read
@@ -11,38 +13,90 @@ concurrency:
group: container-${{ gitea.workflow }}-${{ gitea.ref }} group: container-${{ gitea.workflow }}-${{ gitea.ref }}
cancel-in-progress: true cancel-in-progress: true
env:
REGISTRY: git.sechmachine.io.vn
IMAGE_NAME: sechmachine/labtimesheet
jobs: jobs:
amd64: verify:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30 timeout-minutes: 30
env:
TESTCONTAINERS_HOST_OVERRIDE: host.docker.internal
steps: steps:
- name: Check out source - 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@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
permissions:
contents: read
packages: write
steps:
- name: Check out source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Buildx - name: Set up Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Select image tags - name: Select image tags
id: image id: image
env: env:
CONFIGURED_IMAGE: ${{ vars.CONTAINER_IMAGE }}
EVENT_NAME: ${{ gitea.event_name }} EVENT_NAME: ${{ gitea.event_name }}
GITEA_REF: ${{ gitea.ref }} GITEA_REF: ${{ gitea.ref }}
GITEA_SHA: ${{ gitea.sha }} GITEA_SHA: ${{ gitea.sha }}
run: | run: |
publish=false publish=false
image=labtimesheet image="$REGISTRY/$IMAGE_NAME"
if [ "$EVENT_NAME" = "push" ] && [ "$GITEA_REF" = "refs/heads/main" ]; then if [ "$EVENT_NAME" = "push" ] && [ "$GITEA_REF" = "refs/heads/main" ]; then
test -n "$CONFIGURED_IMAGE" || { echo "Repository variable CONTAINER_IMAGE is required" >&2; exit 1; }
publish=true publish=true
image="$CONFIGURED_IMAGE"
fi fi
{ {
echo "publish=$publish" echo "publish=$publish"
echo "image=$image" echo "image=$image"
echo "registry=${image%%/*}"
echo "tags<<EOF" echo "tags<<EOF"
echo "$image:sha-${GITEA_SHA}-amd64" echo "$image:sha-${GITEA_SHA}-amd64"
if [ "$publish" = "true" ]; then if [ "$publish" = "true" ]; then
@@ -54,14 +108,14 @@ jobs:
- name: Log in to registry - name: Log in to registry
if: steps.image.outputs.publish == 'true' if: steps.image.outputs.publish == 'true'
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with: with:
registry: ${{ steps.image.outputs.registry }} registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }} username: ${{ gitea.actor }}
password: ${{ secrets.REGISTRY_TOKEN }} password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build AMD64 image and publish main - name: Build AMD64 image and publish main
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with: with:
context: . context: .
platforms: linux/amd64 platforms: linux/amd64
@@ -73,42 +127,51 @@ jobs:
arm64: arm64:
# Gitea cannot schedule a probe on a missing label. Enable this repository variable only # 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. # 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 runs-on: ubuntu-latest-arm
timeout-minutes: 30 timeout-minutes: 30
permissions:
contents: read
packages: write
steps: steps:
- name: Check out source - name: Check out source
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Buildx - 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 id: image
env: env:
CONFIGURED_IMAGE: ${{ vars.CONTAINER_IMAGE }} EVENT_NAME: ${{ gitea.event_name }}
GITEA_REF: ${{ gitea.ref }}
GITEA_SHA: ${{ gitea.sha }} GITEA_SHA: ${{ gitea.sha }}
run: | run: |
test -n "$CONFIGURED_IMAGE" || { echo "Repository variable CONTAINER_IMAGE is required" >&2; exit 1; } publish=false
echo "image=$CONFIGURED_IMAGE" >> "$GITHUB_OUTPUT" image="$REGISTRY/$IMAGE_NAME"
echo "registry=${CONFIGURED_IMAGE%%/*}" >> "$GITHUB_OUTPUT" if [ "$EVENT_NAME" = "push" ] && [ "$GITEA_REF" = "refs/heads/main" ]; then
echo "tag=$CONFIGURED_IMAGE:sha-${GITEA_SHA}-arm64" >> "$GITHUB_OUTPUT" publish=true
fi
echo "publish=$publish" >> "$GITHUB_OUTPUT"
echo "tag=$image:sha-${GITEA_SHA}-arm64" >> "$GITHUB_OUTPUT"
- name: Log in to registry - 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: with:
registry: ${{ steps.image.outputs.registry }} registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }} username: ${{ gitea.actor }}
password: ${{ secrets.REGISTRY_TOKEN }} password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build native ARM64 image - name: Build native ARM64 image
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with: with:
context: . context: .
platforms: linux/arm64 platforms: linux/arm64
push: true push: ${{ steps.image.outputs.publish }}
tags: ${{ steps.image.outputs.tag }} tags: ${{ steps.image.outputs.tag }}
build-args: | build-args: |
VCS_REF=${{ gitea.sha }} VCS_REF=${{ gitea.sha }}
@@ -118,28 +181,23 @@ jobs:
needs: [amd64, arm64] needs: [amd64, arm64]
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
permissions:
contents: read
packages: write
steps: steps:
- name: Set up Buildx - 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
env:
CONFIGURED_IMAGE: ${{ vars.CONTAINER_IMAGE }}
run: |
test -n "$CONFIGURED_IMAGE" || { echo "Repository variable CONTAINER_IMAGE is required" >&2; exit 1; }
echo "registry=${CONFIGURED_IMAGE%%/*}" >> "$GITHUB_OUTPUT"
- name: Log in to registry - name: Log in to registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with: with:
registry: ${{ steps.image.outputs.registry }} registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }} username: ${{ gitea.actor }}
password: ${{ secrets.REGISTRY_TOKEN }} password: ${{ secrets.REGISTRY_TOKEN }}
- name: Publish combined manifest - name: Publish combined manifest
env: env:
IMAGE: ${{ vars.CONTAINER_IMAGE }} IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
GITEA_SHA: ${{ gitea.sha }} GITEA_SHA: ${{ gitea.sha }}
run: | run: |
docker buildx imagetools create \ docker buildx imagetools create \
+5 -3
View File
@@ -15,21 +15,23 @@ jobs:
verify: verify:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30 timeout-minutes: 30
env:
TESTCONTAINERS_HOST_OVERRIDE: host.docker.internal
steps: steps:
- name: Check out source - name: Check out source
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Java 25 - name: Set up Java 25
uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4 uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
with: with:
distribution: temurin distribution: temurin
java-version: '25' java-version: '25'
cache: maven cache: maven
- name: Set up Node 24 - name: Set up Node 24
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with: with:
node-version: '24' node-version: '24'
cache: npm cache: npm
+2 -4
View File
@@ -58,12 +58,10 @@ Configure these repository settings:
| Kind | Name | Value | | Kind | Name | Value |
|---|---|---| |---|---|---|
| Variable | `CONTAINER_IMAGE` | Full image name, for example `git.sechmachine.io.vn/sechmachine/labtimesheet` |
| Variable | `ARM64_RUNNER_AVAILABLE` | `true` only while a trusted `ubuntu-latest-arm` runner is registered and online; otherwise omit it or set `false` | | Variable | `ARM64_RUNNER_AVAILABLE` | `true` only while a trusted `ubuntu-latest-arm` runner is registered and online; otherwise omit it or set `false` |
| Secret | `REGISTRY_USERNAME` | Registry user allowed to publish this package | | Secret | `REGISTRY_TOKEN` | Token for the triggering Gitea account with package read/write access |
| 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. The workflow publishes `git.sechmachine.io.vn/sechmachine/labtimesheet` and authenticates as the triggering Gitea account. `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. 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.
+6 -5
View File
@@ -118,11 +118,12 @@ companion record under [`docs/tests`](docs/tests/README.md).
## Continuous integration and production containers ## Continuous integration and production containers
Gitea Actions now verifies every pull request and push. A separate container Gitea Actions verifies every pull request and push. The separate container
workflow builds Linux AMD64 and publishes only from `main`; native Linux ARM64 workflow runs only for a manual dispatch or a push to `main`, and its verification
is added only when the repository explicitly declares that its ARM runner is job must pass before either image build starts. Manual runs build without publishing;
online. Every published revision has an immutable `sha-<full-commit>` tag, with `main` pushes publish Linux AMD64 and add native Linux ARM64 only when the repository
`main` as a convenience alias. 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) 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 supports either a persistent PostgreSQL 18.4 sidecar or an external PostgreSQL
+7 -2
View File
@@ -223,8 +223,9 @@ claim never replaces a test command and result.
Gitea runs the frontend tests/build, complete Maven/PostgreSQL suite, Javadoc, Gitea runs the frontend tests/build, complete Maven/PostgreSQL suite, Javadoc,
generated-asset check, and whitespace check for every pull request and push. generated-asset check, and whitespace check for every pull request and push.
The separate container workflow builds the production Dockerfile without The separate container workflow runs only when manually dispatched or when
publishing pull-request or work-branch images. Only `main` publishes. `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 Run focused and affected tests locally before pushing. CI is the shared
confirmation, not a substitute for local RED and GREEN evidence. 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 Start Docker Desktop or OrbStack. Run `docker version`. OrbStack users should
also check the `DOCKER_HOST` command shown in Section 1. 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 ### The wrong Java version is used
Run `java -version` and `./mvnw -version`. Both should report Java 25. Set 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.
@@ -0,0 +1,63 @@
# Test Evidence: Gitea container registry authentication
- **Test type:** Integration
- **Requirement IDs:** `OPS-012`, `OPS-016`, `OPS-017`
- **Scenario IDs:** `AC-OPS-004`
- **Test class/method:** `src/test/js/delivery-contract.test.mjs``container workflow runs only manually or on main and verifies before either image build`
- **Implementation commit:** `4dd9f96a231316ce2c14755157a380a2123c2f0b`
## Protected behavior
A push to `main` publishes `git.sechmachine.io.vn/sechmachine/labtimesheet` by authenticating the triggering Gitea account with the repository `REGISTRY_TOKEN`. Publication does not depend on separately configured image-name or username settings.
## Test method
The dependency-free Node contract reads the committed workflow and checks its fixed registry/image coordinates, actor-based username, token secret, and absence of the obsolete `CONTAINER_IMAGE` and `REGISTRY_USERNAME` settings. Ruby's YAML parser separately checks workflow syntax.
## Hand-derived expected result
The repository and package location are stable project facts. Therefore the workflow needs one credential only: a token belonging to the triggering actor with package read/write permission. Manual dispatch still builds without publishing; only a `main` push logs in and publishes.
## RED
**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
4 tests ran: 3 passed, 1 failed. The container contract could not find the fixed registry/image or actor-based login. Real Gitea Container run 179 independently failed before registry login with "Repository variable CONTAINER_IMAGE is required", so REGISTRY_TOKEN was never used.
```
## GREEN
**Command**
```text
env PATH=/opt/homebrew/opt/node@24/bin:/usr/bin:/bin node --test src/test/js/delivery-contract.test.mjs
ruby -e 'require "yaml"; YAML.safe_load(File.read(".gitea/workflows/container.yml"), aliases: true); puts "container workflow YAML: OK"'
```
**Observed result**
```text
Delivery contract: 4 tests, 4 passed. Container workflow YAML: OK.
```
## Affected suite
**Command and result**
```text
git diff --check
! rg -n 'CONTAINER_IMAGE|REGISTRY_USERNAME' .gitea/workflows/container.yml DEPLOYMENT.md
Both checks passed. Application tests were deliberately not repeated because the change is limited to workflow metadata, its contract test, and deployment guidance; the container workflow retains its mandatory verify job before building.
```
## External-test boundaries
Local checks do not authenticate to the private registry. The first `main` push containing this change is the production-shaped check of `REGISTRY_TOKEN`, package permissions, and registry publication.
+36 -1
View File
@@ -6,6 +6,14 @@ import test from "node:test";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); const root = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
const read = (path) => readFileSync(resolve(root, path), "utf8"); 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", () => { test("production image and Compose keep one image usable with bundled or external PostgreSQL", () => {
const dockerfile = read("Dockerfile"); const dockerfile = read("Dockerfile");
@@ -37,17 +45,44 @@ test("verification workflow checks every pull request and pushed branch without
assert.match(workflow, /npm run test:ui/); assert.match(workflow, /npm run test:ui/);
assert.match(workflow, /npm run build/); assert.match(workflow, /npm run build/);
assert.match(workflow, /\.\/mvnw -B test/); assert.match(workflow, /\.\/mvnw -B test/);
assert.match(workflow, /TESTCONTAINERS_HOST_OVERRIDE: host\.docker\.internal/);
assert.doesNotMatch(workflow, /permissions:\s*write-all/); 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"); 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, /runs-on: ubuntu-latest-arm/);
assert.match(workflow, /vars\.ARM64_RUNNER_AVAILABLE == 'true'/); assert.match(workflow, /vars\.ARM64_RUNNER_AVAILABLE == 'true'/);
assert.match(workflow, /gitea\.ref == 'refs\/heads\/main'/); assert.match(workflow, /gitea\.ref == 'refs\/heads\/main'/);
assert.match(workflow, /sha-\$\{GITEA_SHA\}-amd64/); assert.match(workflow, /sha-\$\{GITEA_SHA\}-amd64/);
assert.match(workflow, /sha-\$\{GITEA_SHA\}-arm64/); assert.match(workflow, /sha-\$\{GITEA_SHA\}-arm64/);
assert.match(workflow, /imagetools create/); assert.match(workflow, /imagetools create/);
assert.match(workflow, /REGISTRY: git\.sechmachine\.io\.vn/);
assert.match(workflow, /IMAGE_NAME: sechmachine\/labtimesheet/);
assert.match(workflow, /username: \$\{\{ gitea\.actor \}\}/);
assert.match(workflow, /password: \$\{\{ secrets\.REGISTRY_TOKEN \}\}/);
assert.doesNotMatch(workflow, /CONTAINER_IMAGE|REGISTRY_USERNAME/);
assert.doesNotMatch(workflow, /ssh|DEPLOY_HOST|DEPLOY_KEY/i); 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/);
});