Compare commits
2
Commits
d044277194
...
200a17e5bd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
200a17e5bd | ||
|
|
4dd9f96a23 |
@@ -13,6 +13,10 @@ 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:
|
||||||
verify:
|
verify:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -66,6 +70,9 @@ jobs:
|
|||||||
needs: verify
|
needs: verify
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Check out source
|
- name: Check out source
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
@@ -78,22 +85,18 @@ jobs:
|
|||||||
- 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
|
||||||
@@ -107,8 +110,8 @@ jobs:
|
|||||||
if: steps.image.outputs.publish == 'true'
|
if: steps.image.outputs.publish == 'true'
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
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
|
||||||
@@ -128,6 +131,9 @@ jobs:
|
|||||||
needs: verify
|
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
@@ -140,28 +146,24 @@ jobs:
|
|||||||
- name: Select image tag
|
- name: Select image tag
|
||||||
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" >> "$GITHUB_OUTPUT"
|
echo "publish=$publish" >> "$GITHUB_OUTPUT"
|
||||||
echo "registry=${image%%/*}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "tag=$image:sha-${GITEA_SHA}-arm64" >> "$GITHUB_OUTPUT"
|
echo "tag=$image:sha-${GITEA_SHA}-arm64" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- 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@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
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
|
||||||
@@ -179,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@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
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@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
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 \
|
||||||
|
|||||||
+2
-4
@@ -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` 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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -64,6 +64,11 @@ test("container workflow runs only manually or on main and verifies before eithe
|
|||||||
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user