diff --git a/.gitea/workflows/container.yml b/.gitea/workflows/container.yml index 2c2f5f6..1a2b2ef 100644 --- a/.gitea/workflows/container.yml +++ b/.gitea/workflows/container.yml @@ -13,6 +13,10 @@ concurrency: group: container-${{ gitea.workflow }}-${{ gitea.ref }} cancel-in-progress: true +env: + REGISTRY: git.sechmachine.io.vn + IMAGE_NAME: sechmachine/labtimesheet + jobs: verify: runs-on: ubuntu-latest @@ -66,6 +70,9 @@ jobs: 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 @@ -78,22 +85,18 @@ jobs: - name: Select image tags 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 + image="$REGISTRY/$IMAGE_NAME" 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 - image="$CONFIGURED_IMAGE" fi { echo "publish=$publish" echo "image=$image" - echo "registry=${image%%/*}" echo "tags<&2; exit 1; } 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 if: steps.image.outputs.publish == 'true' uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: - registry: ${{ steps.image.outputs.registry }} - username: ${{ secrets.REGISTRY_USERNAME }} + registry: ${{ env.REGISTRY }} + username: ${{ gitea.actor }} password: ${{ secrets.REGISTRY_TOKEN }} - name: Build native ARM64 image @@ -179,28 +181,23 @@ jobs: needs: [amd64, arm64] runs-on: ubuntu-latest timeout-minutes: 10 + permissions: + contents: read + packages: write steps: - name: Set up Buildx 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 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: - registry: ${{ steps.image.outputs.registry }} - username: ${{ secrets.REGISTRY_USERNAME }} + registry: ${{ env.REGISTRY }} + username: ${{ gitea.actor }} password: ${{ secrets.REGISTRY_TOKEN }} - name: Publish combined manifest env: - IMAGE: ${{ vars.CONTAINER_IMAGE }} + IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} GITEA_SHA: ${{ gitea.sha }} run: | docker buildx imagetools create \ diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 820ad42..8c74c54 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -58,12 +58,10 @@ Configure these repository settings: | 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` | -| Secret | `REGISTRY_USERNAME` | Registry user allowed to publish this package | -| Secret | `REGISTRY_TOKEN` | Registry token with package write access | +| Secret | `REGISTRY_TOKEN` | Token for the triggering Gitea account with package read/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-` 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-` 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. diff --git a/docs/tests/integration/container-registry-auth.md b/docs/tests/integration/container-registry-auth.md new file mode 100644 index 0000000..c10574f --- /dev/null +++ b/docs/tests/integration/container-registry-auth.md @@ -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. diff --git a/src/test/js/delivery-contract.test.mjs b/src/test/js/delivery-contract.test.mjs index 82287f2..de7e710 100644 --- a/src/test/js/delivery-contract.test.mjs +++ b/src/test/js/delivery-contract.test.mjs @@ -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\}-arm64/); 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); });