Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion .github/workflows/publish-fragment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
# composition (deploy-kit spec/v1/40-composition.md, spec/v1/55-delivery.md).
#
# release tag -> images built -> this workflow:
# validate -> pack with the release's version -> push -> sign keyless
# validate -> pack with the release's version and the project's share of
# the images lock -> push -> sign keyless
# -> read back and verify -> dispatch composition in the Estate repository
#
# Call it once per project file, after the release's images exist. A merge that
Expand Down Expand Up @@ -48,6 +49,16 @@ on:
required: false
type: string
default: ""
images-lock-artifact:
description: >-
Name of an uploaded artifact holding images.lock.yml: every image this
release's build pushed, by digest, with the user it runs as. The
project's share of it is packed into the fragment. Left empty, the
fragment carries no share and the Platform document's lock must hold
the project's images.
required: false
type: string
default: ""
toolkit-directory:
description: The directory holding the package.json and package-lock.json that pin @jorisjonkers-dev/deploy-kit.
required: false
Expand Down Expand Up @@ -154,9 +165,19 @@ jobs:
}
cp .migration-proof/migration-proof.yml "$(dirname "$PROJECT_FILE")/migration-proof.yml"

- name: Fetch the images lock
if: ${{ inputs.images-lock-artifact != '' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ inputs.images-lock-artifact }}
path: ${{ runner.temp }}/images-lock

- name: Validate and pack
id: pack
env:
# The lock stays outside the checkout: only the project's share of it
# goes into the fragment, never the file itself.
IMAGES_LOCK: ${{ inputs.images-lock-artifact != '' && format('{0}/images-lock/images.lock.yml', runner.temp) || '' }}
PROJECT_FILE: ${{ inputs.project-file }}
VALIDATE_WITH: ${{ inputs.validate-with }}
VERSION: ${{ inputs.version }}
Expand Down
9 changes: 9 additions & 0 deletions actions/publish-fragment/pack.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ set -euo pipefail

: "${PROJECT_FILE:?}" "${VERSION:?}" "${SOURCE_SHA:?}" "${REPOSITORY:?}" "${OUT:?}"
VALIDATE_WITH="${VALIDATE_WITH:-}"
IMAGES_LOCK="${IMAGES_LOCK:-}"
TOOLKIT_DIRECTORY="${TOOLKIT_DIRECTORY:-.}"
DEPLOY_KIT_COMMAND="${DEPLOY_KIT_COMMAND:-npx --no-install deploy-kit}"

Expand All @@ -37,6 +38,13 @@ version="${VERSION#v}"
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "version '${VERSION}' is not a release, vX.Y.Z"
[[ "$SOURCE_SHA" =~ ^[0-9a-f]{40}$ ]] || fail "source-sha '${SOURCE_SHA}' is not a commit"
[ -f "$PROJECT_FILE" ] || fail "no project file at ${PROJECT_FILE}"
# The lock the release's build wrote. The command packs the project's share of
# it and refuses an alias it does not hold.
share=()
if [ -n "$IMAGES_LOCK" ]; then
[ -f "$IMAGES_LOCK" ] || fail "no images lock at ${IMAGES_LOCK}"
share=(--images-lock "$(absolute "$IMAGES_LOCK")")
fi

# The project file and what is read with it: env files, Assets. A directory is
# read as every file below it.
Expand All @@ -60,6 +68,7 @@ deploy_kit publish "$(absolute "$PROJECT_FILE")" \
--repository "$REPOSITORY" \
--source-sha "$SOURCE_SHA" \
--version "$version" \
${share[@]+"${share[@]}"} \
--out "$out"
[ -f "$out/fragment.yml" ] || fail "the command packed no fragment.yml"

Expand Down
29 changes: 29 additions & 0 deletions tests/test_publish_fragment.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,26 @@ def test_validates_then_packs_the_release_and_writes_a_manifest(self):
(self.dir / "output").read_text(), "project=notes\nversion=1.4.0\ninputs-sha=abc123\n"
)

def test_the_images_lock_is_handed_to_the_command_only_when_there_is_one(self):
run = self.run_pack()
self.assertEqual(run.returncode, 0, run.stderr)
self.assertNotIn("--images-lock", self.log.read_text())

lock = self.dir / "built" / "images.lock.yml"
lock.parent.mkdir()
lock.write_text("kind: ImagesLock\n")
self.log.write_text("")
run = self.run_pack(IMAGES_LOCK="built/images.lock.yml")
self.assertEqual(run.returncode, 0, run.stderr)
publish = self.log.read_text().splitlines()[1]
self.assertIn(f"--version 1.4.0 --images-lock {lock} --out {self.dir}/fragment", publish)

def test_a_lock_that_is_named_and_not_there_packs_nothing(self):
run = self.run_pack(IMAGES_LOCK="built/images.lock.yml")
self.assertEqual(run.returncode, 1)
self.assertIn("no images lock at built/images.lock.yml", run.stderr)
self.assertEqual(self.log.read_text() if self.log.exists() else "", "")

def test_a_refused_project_file_packs_nothing(self):
run = self.run_pack(STUB_REFUSE_VALIDATE="1")
self.assertEqual(run.returncode, 1)
Expand Down Expand Up @@ -273,6 +293,15 @@ def test_every_third_party_action_is_pinned_to_a_commit(self):
for uses in re.findall(r"uses: (\S+)", self.text):
self.assertRegex(uses, r"@[0-9a-f]{40}$", uses)

def test_the_lock_is_fetched_outside_the_checkout_and_only_when_named(self):
self.assertIn("if: ${{ inputs.images-lock-artifact != '' }}", self.text)
self.assertIn("path: ${{ runner.temp }}/images-lock", self.text)
self.assertIn(
"IMAGES_LOCK: ${{ inputs.images-lock-artifact != '' && "
"format('{0}/images-lock/images.lock.yml', runner.temp) || '' }}",
self.text,
)

def test_composition_is_started_with_the_dispatch_app_only(self):
self.assertIn("app-id: ${{ vars.ESTATE_DISPATCH_APP_ID }}", self.text)
self.assertIn("repositories: estate", self.text)
Expand Down
Loading