diff --git a/.circleci/config.yml b/.circleci/config.yml index 08acef3..f4df765 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,8 @@ version: 2.1 orbs: - orb-tools: circleci/orb-tools@12.1 + # Pinned to an exact version (K22-CI-11); bump deliberately. + orb-tools: circleci/orb-tools@12.5.0 filters: &filters tags: @@ -13,6 +14,31 @@ tag-only: &tag-only tags: only: /^v[0-9]+\.[0-9]+\.[0-9]+$/ +jobs: + test: + docker: + - image: cimg/node:22.12 + resource_class: medium + steps: + - checkout + - run: + name: Install test tools + command: | + sudo apt-get update -qq + sudo apt-get install -y -qq gettext-base shellcheck >/dev/null + YQ_VERSION=v4.44.6 + sudo curl -fsSL -o /usr/local/bin/yq \ + "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64" + sudo chmod +x /usr/local/bin/yq + git clone -q --depth 1 --branch v1.11.1 https://github.com/bats-core/bats-core.git /tmp/bats-core + sudo /tmp/bats-core/install.sh /usr/local + - run: + name: Script and template tests + command: make test-scripts test-templates + - run: + name: BATS tests + command: bats tests + workflows: lint-pack: jobs: @@ -23,6 +49,8 @@ workflows: - orb-tools/review: filters: *filters exclude: RC006,RC007,RC010 # RC006/RC007: private repo URL unreachable; RC010: machine executors + - test: + filters: *filters test-deploy: jobs: @@ -38,11 +66,15 @@ workflows: - orb-publishing filters: *tag-only + # develop owns the shared dev:snapshot tag. Feature branches publish only an + # immutable dev: tag so open branches cannot overwrite each other's + # snapshot (issue #26). snapshot: jobs: - orb-tools/pack: filters: *filters - orb-tools/publish: + name: publish-develop-snapshot orb_name: kof22/munitor vcs_type: << pipeline.project.type >> pub_type: dev @@ -55,4 +87,17 @@ workflows: branches: only: - develop + - orb-tools/publish: + name: publish-feature-sha + orb_name: kof22/munitor + vcs_type: << pipeline.project.type >> + pub_type: dev + dev_tags: "dev:${CIRCLE_SHA1}" + requires: + - orb-tools/pack + context: + - orb-publishing + filters: + branches: + only: - /feature\/.*/ diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ce5a448 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,26 @@ +# KofTwentyTwo .editorconfig (standards K22-REPO-03, configs/editorconfig). +# Exception: the existing Bash scripts and CircleCI templates use 2-space +# indentation; K22-CODE-SH-02 specifies 3. K22 exception EX-0011. +# Do not mix widths in a file. +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[{Makefile,makefile,*.mk}] +indent_style = tab + +# Shell: shfmt reads these keys (K22-CODE-SH-02), apart from the indent width above. +[*.{sh,bash,bats}] +binary_next_line = true +switch_case_indent = true +space_redirects = true +function_next_line = true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7328439..57cd176 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,9 +2,10 @@ ## Workflow -- **Branches.** Branch `feature/` or `hotfix/` from `develop` and open a pull request back to `develop`. Releases go through `release/X.Y.Z` into `main`; pushing the `vX.Y.Z` tag publishes `kof22/munitor@X.Y.Z`, and every push to `develop` or `feature/*` publishes `dev:snapshot`. Back-merge `main` into `develop` after a release. +- **Branches.** Branch `feature/` or `hotfix/` from `develop` and open a pull request back to `develop`. Releases go through `release/X.Y.Z` into `main`; pushing the `vX.Y.Z` tag publishes `kof22/munitor@X.Y.Z`, and every push to `develop` publishes `dev:snapshot`, and every push to `feature/*` publishes only the immutable `dev:` tag. Back-merge `main` into `develop` after a release. - **Commits.** Conventional Commits (`feat(#N): ...`, `fix: ...`, `docs: ...`), signed (`git commit -S`), with a Developer Certificate of Origin sign-off line (`git commit -s`, which adds `Signed-off-by: Name `). Reference the GitHub issue (`Closes #N`) where there is one. -- **Checks.** Run `make all` (yamllint, shellcheck, pack plus `circleci orb validate`, script and template tests) and `bats tests` (the `.bats` suites) before pushing. CircleCI runs only the orb-tools lint, pack and review jobs; the script, template and BATS tests run locally, so a green CI run does not replace them. +- **Checks.** Run `make all` (yamllint, shellcheck, pack plus `circleci orb validate`, script and template tests) and `bats tests` (the `.bats` suites) before pushing. CircleCI runs the orb-tools lint, pack and review jobs plus a `test` job (`make test-scripts test-templates` and `bats tests`). `circleci orb validate` still runs only locally. +- **Formatting.** Shell scripts and templates use 2-space indentation (see `.editorconfig`); this deviates from K22-CODE-SH-02 under standards exception EX-0011. - **Security.** Report vulnerabilities privately as described in [SECURITY.md](SECURITY.md), never in a public issue. - **AI assistance.** Disclose AI-assisted changes in the pull request description. diff --git a/README.md b/README.md index 5ae0022..81a757b 100644 --- a/README.md +++ b/README.md @@ -520,6 +520,7 @@ Production versions on `main` are unique per commit, even when merges land minut - **Atomic reservation** -- `build-and-test` on `main` reserves `vX.Y.Z` by creating the git tag through the GitHub API before the version is baked into artifacts. Tag creation is create-if-absent, so concurrent pipelines serialize on it: if another commit already owns the tag, the build takes the next patch version. A rerun of the same commit reuses its tag. Existing tags are never moved. Requires the GitHub context on `build-and-test` (present in the generated `production` workflows). - **Immutable image tags** -- images carry an `org.opencontainers.image.revision` label. `docker-build-push` refuses to push a version tag (main or supplemental) that already exists for a different commit, and fails closed if the registry cannot be inspected. Environment tags such as `develop` stay mutable. - **Ownership-checked GitOps bump** -- `update-cd-repo` verifies that `vX.Y.Z` points at the pipeline's commit before writing the version to the CD repository. +- **Signed GitOps commits** -- `update-cd-repo` creates the CD repository commit through the GitHub GraphQL API (`createCommitOnBranch`) instead of `git push`. GitHub signs API-created commits, so they show as Verified and pass a CD repository ruleset that requires signed commits, with no bypass actor. The commit is authored by the identity behind `GITHUB_TOKEN`; the source repository and commit are recorded in the commit body. Concurrent bumps retry against the new branch head (`expectedHeadOid`). The `ci_git_email`/`ci_git_name` parameters and `org.ci_email`/`org.ci_name` no longer affect these commits. - **Release targets the built commit** -- `github-release` targets `CIRCLE_SHA1`, not the moving `main` head, and refuses if the tag points elsewhere. `cd_path` is rendered into generated pipelines only when `cd.path` is set, so manifests that pin an older `orb_version` keep validating. diff --git a/src/commands/update_cd_repo.yml b/src/commands/update_cd_repo.yml index 8a7c72c..2e9937b 100644 --- a/src/commands/update_cd_repo.yml +++ b/src/commands/update_cd_repo.yml @@ -39,11 +39,14 @@ parameters: ci_git_email: type: string default: "" - description: Git author email for CD repo commits. + description: > + Deprecated and ignored. GitOps commits are created through the GitHub API, + signed by GitHub and authored by the GITHUB_TOKEN identity. ci_git_name: type: string default: "" - description: Git author name for CD repo commits. + description: > + Deprecated and ignored. See ci_git_email. supplemental_images: type: string default: "[]" diff --git a/src/jobs/update_cd_repo.yml b/src/jobs/update_cd_repo.yml index 38c33a3..3a3e7a3 100644 --- a/src/jobs/update_cd_repo.yml +++ b/src/jobs/update_cd_repo.yml @@ -31,11 +31,14 @@ parameters: ci_git_email: type: string default: "" - description: Git author email for CD repo commits. + description: > + Deprecated and ignored. GitOps commits are created through the GitHub API, + signed by GitHub and authored by the GITHUB_TOKEN identity. ci_git_name: type: string default: "" - description: Git author name for CD repo commits. + description: > + Deprecated and ignored. See ci_git_email. supplemental_images: type: string default: "[]" diff --git a/src/scripts/generate_config.sh b/src/scripts/generate_config.sh index 21100df..07e0b65 100755 --- a/src/scripts/generate_config.sh +++ b/src/scripts/generate_config.sh @@ -97,7 +97,7 @@ validate_required_fields "${PIPELINE_TYPE}" "${CONFIG_FILE}" # -------------------------------------------------------------------------- # Warn about unknown top-level keys (catches typos like java_verion) # -------------------------------------------------------------------------- -KNOWN_KEYS="pipeline orb_version image_name server_module java_version node_version sonar docker cd coverage e2e sbom owasp contexts npm node services test terraform sast health kustomize kube_linter_config package_manager yamllint_paths supplemental_images" +KNOWN_KEYS="pipeline orb_version org image_name server_module java_version node_version sonar docker cd coverage e2e sbom owasp contexts npm node services test terraform sast health kustomize kube_linter_config package_manager yamllint_paths supplemental_images" ACTUAL_KEYS=$(yq 'keys | .[]' "${CONFIG_FILE}" 2>/dev/null || true) for key in ${ACTUAL_KEYS}; do if ! echo "${KNOWN_KEYS}" | grep -qw "${key}"; then diff --git a/src/scripts/packed_generate_config.sh b/src/scripts/packed_generate_config.sh index d4a83f3..1bdee91 100755 --- a/src/scripts/packed_generate_config.sh +++ b/src/scripts/packed_generate_config.sh @@ -3249,7 +3249,7 @@ validate_required_fields "${PIPELINE_TYPE}" "${CONFIG_FILE}" # -------------------------------------------------------------------------- # Warn about unknown top-level keys (catches typos like java_verion) # -------------------------------------------------------------------------- -KNOWN_KEYS="pipeline orb_version image_name server_module java_version node_version sonar docker cd coverage e2e sbom owasp contexts npm node services test terraform sast health kustomize kube_linter_config package_manager yamllint_paths supplemental_images" +KNOWN_KEYS="pipeline orb_version org image_name server_module java_version node_version sonar docker cd coverage e2e sbom owasp contexts npm node services test terraform sast health kustomize kube_linter_config package_manager yamllint_paths supplemental_images" ACTUAL_KEYS=$(yq 'keys | .[]' "${CONFIG_FILE}" 2>/dev/null || true) for key in ${ACTUAL_KEYS}; do if ! echo "${KNOWN_KEYS}" | grep -qw "${key}"; then diff --git a/src/scripts/update_cd_repo.sh b/src/scripts/update_cd_repo.sh index a7e68ec..2718814 100755 --- a/src/scripts/update_cd_repo.sh +++ b/src/scripts/update_cd_repo.sh @@ -68,128 +68,159 @@ git clone "https://x-access-token:${GITHUB_TOKEN}@github.com/${REPO}.git" "${CLO cd "${CLONE_DIR}" -if [[ "${FORMAT}" == "kustomize" ]]; then - # --- Kustomize mode --- - if [[ -z "${IMAGE_NAME}" ]]; then - echo "ERROR: cd_image_name is required for Kustomize repos." - exit 1 - fi - - # Explicit paths support consolidated GitOps repositories. Existing dedicated - # repositories retain the overlays/ convention by default. - if [[ -n "${CONFIGURED_PATH}" ]]; then - if [[ "${CONFIGURED_PATH}" == /* || "${CONFIGURED_PATH}" == *".."* || "${CONFIGURED_PATH}" != *kustomization.yaml ]]; then - echo "ERROR: cd.path must be a relative path to a kustomization.yaml file without '..'." +apply_updates() { + if [[ "${FORMAT}" == "kustomize" ]]; then + # --- Kustomize mode --- + if [[ -z "${IMAGE_NAME}" ]]; then + echo "ERROR: cd_image_name is required for Kustomize repos." exit 1 fi - KUST_FILE="${CONFIGURED_PATH}" - elif [[ -n "${ENVIRONMENT}" ]]; then - KUST_FILE="overlays/${ENVIRONMENT}/kustomization.yaml" + + # Explicit paths support consolidated GitOps repositories. Existing dedicated + # repositories retain the overlays/ convention by default. + if [[ -n "${CONFIGURED_PATH}" ]]; then + if [[ "${CONFIGURED_PATH}" == /* || "${CONFIGURED_PATH}" == *".."* || "${CONFIGURED_PATH}" != *kustomization.yaml ]]; then + echo "ERROR: cd.path must be a relative path to a kustomization.yaml file without '..'." + exit 1 + fi + KUST_FILE="${CONFIGURED_PATH}" + elif [[ -n "${ENVIRONMENT}" ]]; then + KUST_FILE="overlays/${ENVIRONMENT}/kustomization.yaml" + else + KUST_FILE="kustomization.yaml" + fi + + if [[ ! -f "${KUST_FILE}" ]]; then + echo "ERROR: Kustomize file '${KUST_FILE}' not found in CD repo." + if [[ -z "${CONFIGURED_PATH}" ]]; then + echo "Available overlays:" + ls overlays/ 2>/dev/null || echo " (no overlays directory)" + fi + exit 1 + fi + + echo "Updating ${KUST_FILE}: ${IMAGE_NAME} -> ${VERSION}" + if ! yq -e ".images[] | select(.name == \"${IMAGE_NAME}\")" "${KUST_FILE}" >/dev/null; then + echo "ERROR: Image '${IMAGE_NAME}' is not declared in ${KUST_FILE}." + exit 1 + fi + yq -i "(.images[] | select(.name == \"${IMAGE_NAME}\")).newTag = \"${VERSION}\"" "${KUST_FILE}" + yq -i "del((.images[] | select(.name == \"${IMAGE_NAME}\")).digest)" "${KUST_FILE}" + git add "${KUST_FILE}" + else - KUST_FILE="kustomization.yaml" - fi + # --- Helm mode (default) --- + echo "Updating ${KEY} -> ${VERSION}" - if [[ ! -f "${KUST_FILE}" ]]; then - echo "ERROR: Kustomize file '${KUST_FILE}' not found in CD repo." - if [[ -z "${CONFIGURED_PATH}" ]]; then - echo "Available overlays:" - ls overlays/ 2>/dev/null || echo " (no overlays directory)" - fi - exit 1 - fi - - echo "Updating ${KUST_FILE}: ${IMAGE_NAME} -> ${VERSION}" - if ! yq -e ".images[] | select(.name == \"${IMAGE_NAME}\")" "${KUST_FILE}" >/dev/null; then - echo "ERROR: Image '${IMAGE_NAME}' is not declared in ${KUST_FILE}." - exit 1 - fi - yq -i "(.images[] | select(.name == \"${IMAGE_NAME}\")).newTag = \"${VERSION}\"" "${KUST_FILE}" - yq -i "del((.images[] | select(.name == \"${IMAGE_NAME}\")).digest)" "${KUST_FILE}" - git add "${KUST_FILE}" - -else - # --- Helm mode (default) --- - echo "Updating ${KEY} -> ${VERSION}" - - # Resolve environment-specific values path when CD_ENVIRONMENT is set - if [[ -n "${ENVIRONMENT}" ]]; then - ENV_VALUES="environments/${ENVIRONMENT}/${VALUES}" - if [[ -f "${ENV_VALUES}" ]]; then - VALUES="${ENV_VALUES}" - echo "Using environment path: ${VALUES}" - else - echo "Environment path '${ENV_VALUES}' not found, falling back to root '${VALUES}'" - fi - fi - - if [[ ! -f "${VALUES}" ]]; then - echo "ERROR: Values file '${VALUES}' not found in CD repo." - exit 1 - fi - - yq -i ".${KEY} = \"${VERSION}\"" "${VALUES}" - git add "${VALUES}" -fi - -# Update supplemental image tags (if any) -SUPP_JSON="${SUPPLEMENTAL_IMAGES_JSON:-[]}" -if [[ "${SUPP_JSON}" != "[]" && "${SUPP_JSON}" != "null" ]]; then - SUPP_COUNT=$(echo "${SUPP_JSON}" | jq 'length') - echo "Updating ${SUPP_COUNT} supplemental image tag(s)..." - - for ((i = 0; i < SUPP_COUNT; i++)); do - SUPP_NAME=$(echo "${SUPP_JSON}" | jq -r ".[$i].name") - - if [[ "${FORMAT}" == "kustomize" ]]; then - SUPP_CD_IMAGE=$(echo "${SUPP_JSON}" | jq -r ".[$i].cd.image_name // \"\"") - if [[ -z "${SUPP_CD_IMAGE}" ]]; then - SUPP_CD_IMAGE="${IMAGE_NAME}-${SUPP_NAME}" - fi - echo " Kustomize: ${SUPP_CD_IMAGE} -> ${VERSION}" - # Upsert: add entry if it doesn't exist, then update - EXISTING=$(yq ".images[] | select(.name == \"${SUPP_CD_IMAGE}\") | .name" "${KUST_FILE}") - if [[ -z "${EXISTING}" ]]; then - echo " (creating new image entry)" - yq -i ".images += [{\"name\": \"${SUPP_CD_IMAGE}\", \"newTag\": \"${VERSION}\"}]" "${KUST_FILE}" + # Resolve environment-specific values path when CD_ENVIRONMENT is set + if [[ -n "${ENVIRONMENT}" ]]; then + ENV_VALUES="environments/${ENVIRONMENT}/${VALUES}" + if [[ -f "${ENV_VALUES}" ]]; then + VALUES="${ENV_VALUES}" + echo "Using environment path: ${VALUES}" else - yq -i "(.images[] | select(.name == \"${SUPP_CD_IMAGE}\")).newTag = \"${VERSION}\"" "${KUST_FILE}" - yq -i "del((.images[] | select(.name == \"${SUPP_CD_IMAGE}\")).digest)" "${KUST_FILE}" - fi - git add "${KUST_FILE}" - else - SUPP_IMAGE_KEY=$(echo "${SUPP_JSON}" | jq -r ".[$i].cd.image_key // \"\"") - if [[ -n "${SUPP_IMAGE_KEY}" ]]; then - echo " Helm: ${SUPP_IMAGE_KEY} -> ${VERSION}" - yq -i ".${SUPP_IMAGE_KEY} = \"${VERSION}\"" "${VALUES}" - git add "${VALUES}" + echo "Environment path '${ENV_VALUES}' not found, falling back to root '${VALUES}'" fi fi - done -fi -# Commit and push -git config user.email "${CI_GIT_EMAIL:-${MUNITOR_CI_EMAIL:-munitor-ci@koftwentytwo.com}}" -git config user.name "${CI_GIT_NAME:-${MUNITOR_CI_NAME:-Munitor CI}}" + if [[ ! -f "${VALUES}" ]]; then + echo "ERROR: Values file '${VALUES}' not found in CD repo." + exit 1 + fi -if git diff --cached --quiet; then - echo "No changes to commit (version already up to date)." - exit 0 -fi + yq -i ".${KEY} = \"${VERSION}\"" "${VALUES}" + git add "${VALUES}" + fi -git commit -m "chore: update ${IMAGE_NAME:-image} to ${VERSION}" -m "Source: ${CIRCLE_PROJECT_REPONAME:-unknown}@${CIRCLE_SHA1:-unknown}" + # Update supplemental image tags (if any) + SUPP_JSON="${SUPPLEMENTAL_IMAGES_JSON:-[]}" + if [[ "${SUPP_JSON}" != "[]" && "${SUPP_JSON}" != "null" ]]; then + SUPP_COUNT=$(echo "${SUPP_JSON}" | jq 'length') + echo "Updating ${SUPP_COUNT} supplemental image tag(s)..." + + for ((i = 0; i < SUPP_COUNT; i++)); do + SUPP_NAME=$(echo "${SUPP_JSON}" | jq -r ".[$i].name") + + if [[ "${FORMAT}" == "kustomize" ]]; then + SUPP_CD_IMAGE=$(echo "${SUPP_JSON}" | jq -r ".[$i].cd.image_name // \"\"") + if [[ -z "${SUPP_CD_IMAGE}" ]]; then + SUPP_CD_IMAGE="${IMAGE_NAME}-${SUPP_NAME}" + fi + echo " Kustomize: ${SUPP_CD_IMAGE} -> ${VERSION}" + # Upsert: add entry if it doesn't exist, then update + EXISTING=$(yq ".images[] | select(.name == \"${SUPP_CD_IMAGE}\") | .name" "${KUST_FILE}") + if [[ -z "${EXISTING}" ]]; then + echo " (creating new image entry)" + yq -i ".images += [{\"name\": \"${SUPP_CD_IMAGE}\", \"newTag\": \"${VERSION}\"}]" "${KUST_FILE}" + else + yq -i "(.images[] | select(.name == \"${SUPP_CD_IMAGE}\")).newTag = \"${VERSION}\"" "${KUST_FILE}" + yq -i "del((.images[] | select(.name == \"${SUPP_CD_IMAGE}\")).digest)" "${KUST_FILE}" + fi + git add "${KUST_FILE}" + else + SUPP_IMAGE_KEY=$(echo "${SUPP_JSON}" | jq -r ".[$i].cd.image_key // \"\"") + if [[ -n "${SUPP_IMAGE_KEY}" ]]; then + echo " Helm: ${SUPP_IMAGE_KEY} -> ${VERSION}" + yq -i ".${SUPP_IMAGE_KEY} = \"${VERSION}\"" "${VALUES}" + git add "${VALUES}" + fi + fi + done + fi +} + +# Commit through the GitHub GraphQL API (createCommitOnBranch) rather than pushing local commits. +# GitHub signs API-created commits, so they show as Verified and satisfy a +# "require signed commits" ruleset on the GitOps branch without a bypass actor. +# The commit author is the GITHUB_TOKEN identity; the source build is recorded +# in the commit body. expectedHeadOid makes concurrent updates fail cleanly and +# retry against the new head instead of overwriting each other. +BRANCH="$(git branch --show-current)" +HEADLINE="chore: update ${IMAGE_NAME:-image} to ${VERSION}" +MESSAGE_BODY="Source: ${CIRCLE_PROJECT_REPONAME:-unknown}@${CIRCLE_SHA1:-unknown}" + +commit_via_api() { + local head_oid additions payload response file + head_oid="$(git rev-parse HEAD)" + additions='[]' + while IFS= read -r file; do + base64 -w0 "${file}" > /tmp/cd-file.b64 + additions="$(jq --arg path "${file}" --rawfile contents /tmp/cd-file.b64 '. + [{path: $path, contents: $contents}]' <<< "${additions}")" + done < <(git diff --cached --name-only) + payload="$(jq -n --arg repo "${REPO}" --arg branch "${BRANCH}" --arg oid "${head_oid}" --arg headline "${HEADLINE}" --arg body "${MESSAGE_BODY}" --argjson additions "${additions}" '{query: "mutation($input: CreateCommitOnBranchInput!) { createCommitOnBranch(input: $input) { commit { oid url } } }", + variables: {input: {branch: {repositoryNameWithOwner: $repo, branchName: $branch}, + expectedHeadOid: $oid, message: {headline: $headline, body: $body}, + fileChanges: {additions: $additions}}}}')" + if ! response="$(curl -sS -X POST -H "Authorization: bearer ${GITHUB_TOKEN}" -H "Content-Type: application/json" https://api.github.com/graphql --data @- <<< "${payload}")"; then + echo "GraphQL request failed." + return 1 + fi + if [[ "$(jq -r '.data.createCommitOnBranch.commit.oid // empty' <<< "${response}")" == "" ]]; then + echo "GraphQL commit rejected: $(jq -c '.errors // .' <<< "${response}")" + return 1 + fi + echo "Signed commit: $(jq -r '.data.createCommitOnBranch.commit.url' <<< "${response}")" +} -# Independent pipelines may update a shared GitOps repository concurrently. for attempt in 1 2 3; do - if git push origin HEAD; then + apply_updates + + if git diff --cached --quiet; then + echo "No changes to commit (version already up to date)." + exit 0 + fi + + if commit_via_api; then echo "CD repo updated successfully." exit 0 fi if [[ "${attempt}" == "3" ]]; then - echo "ERROR: Could not push the GitOps update after ${attempt} attempts." + echo "ERROR: Could not commit the GitOps update after ${attempt} attempts." exit 1 fi - echo "Push raced with another GitOps update; rebasing before retry ${attempt}/3." - git pull --rebase origin "$(git branch --show-current)" + echo "Commit raced with another GitOps update; refreshing before retry ${attempt}/3." + git fetch -q origin "${BRANCH}" + git reset -q --hard "origin/${BRANCH}" done diff --git a/tests/test_scripts.sh b/tests/test_scripts.sh index 3ce611c..6db9f13 100755 --- a/tests/test_scripts.sh +++ b/tests/test_scripts.sh @@ -328,12 +328,21 @@ else fail "should support and validate an explicit Kustomize path" fi -echo -n " TEST: retries concurrent shared-repository pushes... " +echo -n " TEST: retries concurrent shared-repository commits against the new head... " if grep -q 'for attempt in 1 2 3' "${SRC_SCRIPTS}/update_cd_repo.sh" && - grep -q 'git pull --rebase' "${SRC_SCRIPTS}/update_cd_repo.sh"; then + grep -q 'expectedHeadOid' "${SRC_SCRIPTS}/update_cd_repo.sh" && + grep -q 'git reset -q --hard "origin/${BRANCH}"' "${SRC_SCRIPTS}/update_cd_repo.sh"; then pass else - fail "should rebase and retry shared-repository push races" + fail "should retry commit races against the refreshed branch head" +fi + +echo -n " TEST: commits through the signed GraphQL API, never git push... " +if grep -q 'createCommitOnBranch' "${SRC_SCRIPTS}/update_cd_repo.sh" && + ! grep -q 'git push' "${SRC_SCRIPTS}/update_cd_repo.sh"; then + pass +else + fail "should create GitOps commits with createCommitOnBranch so GitHub signs them" fi echo -n " TEST: deletes digest field when setting newTag (kustomize)... "