fix(ci): use Gitea actor for registry publishing

This commit is contained in:
sechmachine
2026-08-16 00:28:15 +07:00
parent d044277194
commit 4dd9f96a23
4 changed files with 92 additions and 29 deletions
+22 -25
View File
@@ -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
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` 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:** `pending`
## 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.
+5
View File
@@ -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);
}); });