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
26 changes: 23 additions & 3 deletions .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,18 @@ on:
- master
paths:
- buildinfo.json
- build.py
- 'docker/**'
workflow_call:
inputs:
previous_sha:
type: string
required: false
default: ''
checkout_ref:
type: string
required: false
default: ''
# workaround for #526
workflow_dispatch:

Expand All @@ -18,15 +29,24 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ github.ref_name }}
ref: ${{ inputs.checkout_ref || github.sha }}
fetch-depth: 0

- name: Set up QEMU
uses: docker/setup-qemu-action@v4

- name: build and push all images
- name: Build and push changed images
if: ${{ env.DOCKER_USERNAME != '' && env.DOCKER_PASSWORD != '' }}
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
PREVIOUS_SHA: ${{ inputs.previous_sha || github.event.before }}
run: |
./build.py --push-tags --multiarch --both
args=(--push-tags --multiarch --both)
# Runtime/build changes and manual runs rebuild all current versions.
if [[ -n "$PREVIOUS_SHA" ]] && git cat-file -e "$PREVIOUS_SHA^{commit}" 2>/dev/null; then
if git diff --quiet "$PREVIOUS_SHA" HEAD -- docker build.py; then
args+=(--changed-from "$PREVIOUS_SHA")
fi
fi
./build.py "${args[@]}"
6 changes: 6 additions & 0 deletions .github/workflows/update.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ jobs:
if: github.event_name != 'schedule' || (github.event_name == 'schedule' && github.repository == 'factoriotools/factorio-docker')
outputs:
updated: ${{ steps.changes.outputs.updated }}
before_sha: ${{ steps.before.outputs.sha }}
head_sha: ${{ steps.changes.outputs.sha }}

steps:
- name: Checkout
Expand All @@ -27,6 +29,7 @@ jobs:
- name: Check for changes
id: changes
run: |
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
if [ "$(git rev-parse HEAD)" != "${{ steps.before.outputs.sha }}" ]; then
echo "updated=true" >> "$GITHUB_OUTPUT"
else
Expand All @@ -37,4 +40,7 @@ jobs:
needs: check
if: needs.check.outputs.updated == 'true'
uses: ./.github/workflows/docker-build.yml
with:
previous_sha: ${{ needs.check.outputs.before_sha }}
checkout_ref: ${{ needs.check.outputs.head_sha }}
secrets: inherit
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,14 @@ The `PRESET` environment variable is used when generating a new map (when `GENER

If PRESET is not specified or left empty, the map will be generated using the settings from `map-gen-settings.json` and `map-settings.json` without a preset.

## Building images

Normal deployments should pull published images rather than build them. For development, run `python3 build.py --both` from the repository root to build regular and rootless variants. Add `--multiarch --push-tags` only when you intend to publish images and have registry credentials configured.

Use `--changed-from GIT_REF` to build only releases with new versions, changed archive checksums or added tags. Removing a tag alone does not trigger a rebuild. Automatic release updates use this mode; runtime/build-source changes and manual workflow runs rebuild all current versions.

Omit `--changed-from` to retry a failed publication or refresh base layers.

## Container Details

The philosophy is to [keep it simple](http://wiki.c2.com/?KeepItSimple).
Expand Down
51 changes: 39 additions & 12 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,27 @@ def generate_rootless_tags(original_tags):
return [f"{tag}-rootless" for tag in original_tags]


def load_previous_buildinfo(ref):
"""Read release metadata at a git ref without changing the checkout."""
contents = subprocess.check_output(
["git", "show", "--end-of-options", f"{ref}:buildinfo.json"],
cwd=os.path.dirname(os.path.abspath(__file__)), text=True,
)
data = json.loads(contents)
if not isinstance(data, dict):
raise ValueError("Previous buildinfo.json must be an object")
return data


def release_changed(version, info, previous):
old = previous.get(version)
if old is None:
return True
# Removing an alias needs no rebuild: the new owner will publish that tag.
return (any(info.get(key) != old.get(key) for key in ("sha256", "sha256_arm64"))
or bool(set(info["tags"]) - set(old["tags"])))


def main():
parser = argparse.ArgumentParser(description='Build Factorio Docker images')
parser.add_argument('--push-tags', action='store_true', help='Push images to Docker Hub')
Expand All @@ -108,6 +129,8 @@ def main():
parser.add_argument('--only-stable-latest', action='store_true',
help='Build only stable and latest versions (for rootless by default)')

parser.add_argument('--changed-from', metavar='GIT_REF',
help='Build only versions with new archives or added tags since this ref')
args = parser.parse_args()

# Default behavior: build regular images unless specified otherwise
Expand All @@ -117,19 +140,32 @@ def main():
with open(os.path.join(os.path.dirname(__file__), "buildinfo.json")) as file_handle:
builddata = json.load(file_handle)

if args.push_tags:
login()
previous = None
if args.changed_from:
try:
previous = load_previous_buildinfo(args.changed_from)
except (subprocess.CalledProcessError, OSError, ValueError) as error:
parser.error(f"Cannot read previous release metadata: {error}")

# Filter versions if needed
versions_to_build = []
for version, buildinfo in sorted(builddata.items(), key=lambda item: item[0], reverse=True):
if previous is not None and not release_changed(version, buildinfo, previous):
continue
if args.only_stable_latest or (build_rootless and not build_regular):
# For rootless-only builds, default to stable/latest only
if "stable" in buildinfo["tags"] or "latest" in buildinfo["tags"]:
versions_to_build.append((version, buildinfo))
else:
versions_to_build.append((version, buildinfo))

if not versions_to_build:
print("No changed releases to build.")
return

if args.push_tags:
login()

# Build regular images
if build_regular:
print("Building regular images...")
Expand All @@ -142,16 +178,7 @@ def main():
# Build rootless images
if build_rootless:
print("Building rootless images...")
# For rootless, only build stable and latest unless building both
rootless_versions = []
if not build_regular or args.only_stable_latest:
for version, buildinfo in builddata.items():
if "stable" in buildinfo["tags"] or "latest" in buildinfo["tags"]:
rootless_versions.append((version, buildinfo))
else:
rootless_versions = versions_to_build

for version, buildinfo in rootless_versions:
for version, buildinfo in versions_to_build:
sha256 = buildinfo["sha256"]
original_tags = buildinfo["tags"]
rootless_tags = generate_rootless_tags(original_tags)
Expand Down
Loading