diff --git a/README.md b/README.md index e674be3..add9590 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ Installs the [Seriatim](https://gitea.maximumdirect.net/eric/seriatim) transcript processor from source. The role supports Ubuntu 26.04 on `amd64` and `arm64` hosts and is independently usable outside the D&D pipeline role. -The role manages its build dependencies and a root-owned source checkout. At -this point binary installation is added by subsequent implementation work. +The role manages its build dependencies and a root-owned source checkout, +builds a native static executable, validates it, and safely installs it. ## Requirements @@ -49,6 +49,29 @@ provided independently through the managed host's Git configuration or SSH environment; this role does not manage credentials or accept unknown host keys. +## Build and installation behavior + +Builds run from the selected checkout with `GOWORK=off`, +`GOTOOLCHAIN=local`, `CGO_ENABLED=0`, the configured ordered build flags, and +the upstream version-injection linker flags. The resulting executable is +staged and must report the exact configured version before it atomically +replaces `seriatim_binary_path`. It is installed as `root:root` mode `0755`. + +The role records readable inputs, their SHA-256 fingerprint, and the installed +binary checksum beneath `seriatim_build_state_dir`. A source commit, compiler +identity, Go path, version, flags, linker flags, package, or fixed environment +change triggers a rebuild. Missing or corrupt state, a modified binary, or an +incorrect installed version also triggers rebuilding. Failed compilation or +staged validation leaves the previous installed binary and successful state +untouched. + +For example, development from a branch must identify both source and output: + +```yaml +seriatim_source_ref: feature/example +seriatim_build_version: v1.5.0-dev +``` + ## Static validation ```sh diff --git a/tasks/build.yml b/tasks/build.yml new file mode 100644 index 0000000..289976e --- /dev/null +++ b/tasks/build.yml @@ -0,0 +1,189 @@ +--- +- name: Construct canonical Seriatim build inputs + ansible.builtin.set_fact: + seriatim_build_inputs: >- + {{ { + 'source_commit': seriatim_source_commit, + 'go_version': seriatim_go_version_result.stdout, + 'go_binary': seriatim_go_binary, + 'build_version': seriatim_build_version, + 'build_flags': seriatim_go_build_flags, + 'ldflags': seriatim_go_ldflags, + 'package': seriatim_go_package, + 'environment': { + 'GOWORK': 'off', + 'GOTOOLCHAIN': 'local', + 'CGO_ENABLED': '0' + } + } | to_nice_json(indent=2, sort_keys=true) }} + +- name: Calculate desired Seriatim build fingerprint + ansible.builtin.set_fact: + seriatim_desired_fingerprint: "{{ seriatim_build_inputs | hash('sha256') }}" + seriatim_stored_fingerprint: "" + seriatim_stored_checksum: "" + +- name: Inspect installed Seriatim binary + ansible.builtin.stat: + path: "{{ seriatim_binary_path }}" + checksum_algorithm: sha256 + register: seriatim_installed_binary_stat + +- name: Inspect Seriatim build input state + ansible.builtin.stat: + path: "{{ seriatim_build_state_dir }}/build-inputs.json" + register: seriatim_build_inputs_state_stat + +- name: Inspect Seriatim fingerprint state + ansible.builtin.stat: + path: "{{ seriatim_build_state_dir }}/fingerprint.sha256" + register: seriatim_fingerprint_state_stat + +- name: Inspect Seriatim checksum state + ansible.builtin.stat: + path: "{{ seriatim_build_state_dir }}/binary.sha256" + register: seriatim_checksum_state_stat + +- name: Read stored Seriatim fingerprint + ansible.builtin.slurp: + src: "{{ seriatim_build_state_dir }}/fingerprint.sha256" + register: seriatim_stored_fingerprint_result + when: seriatim_fingerprint_state_stat.stat.exists + +- name: Read stored Seriatim checksum + ansible.builtin.slurp: + src: "{{ seriatim_build_state_dir }}/binary.sha256" + register: seriatim_stored_checksum_result + when: seriatim_checksum_state_stat.stat.exists + +- name: Record stored Seriatim state values + ansible.builtin.set_fact: + seriatim_stored_fingerprint: >- + {{ seriatim_stored_fingerprint_result.content | b64decode | trim }} + seriatim_stored_checksum: >- + {{ seriatim_stored_checksum_result.content | b64decode | trim }} + when: + - seriatim_fingerprint_state_stat.stat.exists + - seriatim_checksum_state_stat.stat.exists + +- name: Read installed Seriatim version + ansible.builtin.command: + argv: + - "{{ seriatim_binary_path }}" + - --version + register: seriatim_installed_version_result + changed_when: false + failed_when: false + when: seriatim_installed_binary_stat.stat.exists + +- name: Decide whether Seriatim requires rebuilding + ansible.builtin.set_fact: + seriatim_build_required: >- + {{ not seriatim_installed_binary_stat.stat.exists + or not seriatim_build_inputs_state_stat.stat.exists + or not seriatim_fingerprint_state_stat.stat.exists + or not seriatim_checksum_state_stat.stat.exists + or seriatim_stored_fingerprint != seriatim_desired_fingerprint + or seriatim_stored_checksum != (seriatim_installed_binary_stat.stat.checksum | default('')) + or (seriatim_installed_version_result.rc | default(1)) != 0 + or (seriatim_installed_version_result.stdout | default('')) != 'seriatim version ' ~ seriatim_build_version }} + +- name: Build, validate, and install Seriatim + when: seriatim_build_required + block: + - name: Create temporary Seriatim build directory + ansible.builtin.tempfile: + state: directory + prefix: ansible-seriatim-build- + register: seriatim_build_tempdir + + - name: Build staged Seriatim binary + ansible.builtin.command: + argv: >- + {{ [seriatim_go_binary, 'build'] + + seriatim_go_build_flags + + ['-ldflags', seriatim_go_ldflags, + '-o', seriatim_build_tempdir.path ~ '/seriatim', + seriatim_go_package] }} + chdir: "{{ seriatim_source_dir }}" + environment: + GOWORK: "off" + GOTOOLCHAIN: local + CGO_ENABLED: "0" + changed_when: true + + - name: Validate staged Seriatim binary + ansible.builtin.command: + argv: + - "{{ seriatim_build_tempdir.path }}/seriatim" + - --version + register: seriatim_staged_version_result + changed_when: false + failed_when: >- + seriatim_staged_version_result.rc != 0 or + seriatim_staged_version_result.stdout != 'seriatim version ' ~ seriatim_build_version + + - name: Install validated Seriatim binary + ansible.builtin.copy: + src: "{{ seriatim_build_tempdir.path }}/seriatim" + dest: "{{ seriatim_binary_path }}" + remote_src: true + owner: root + group: root + mode: "0755" + + - name: Validate installed Seriatim binary + ansible.builtin.command: + argv: + - "{{ seriatim_binary_path }}" + - --version + register: seriatim_new_installed_version_result + changed_when: false + failed_when: >- + seriatim_new_installed_version_result.rc != 0 or + seriatim_new_installed_version_result.stdout != 'seriatim version ' ~ seriatim_build_version + + - name: Calculate installed Seriatim checksum + ansible.builtin.stat: + path: "{{ seriatim_binary_path }}" + checksum_algorithm: sha256 + register: seriatim_new_binary_stat + + - name: Create Seriatim build state directory + ansible.builtin.file: + path: "{{ seriatim_build_state_dir }}" + state: directory + owner: root + group: root + mode: "0755" + + - name: Record Seriatim build inputs + ansible.builtin.copy: + content: "{{ seriatim_build_inputs }}\n" + dest: "{{ seriatim_build_state_dir }}/build-inputs.json" + owner: root + group: root + mode: "0644" + + - name: Record Seriatim build fingerprint + ansible.builtin.copy: + content: "{{ seriatim_desired_fingerprint }}\n" + dest: "{{ seriatim_build_state_dir }}/fingerprint.sha256" + owner: root + group: root + mode: "0644" + + - name: Record installed Seriatim checksum + ansible.builtin.copy: + content: "{{ seriatim_new_binary_stat.stat.checksum }}\n" + dest: "{{ seriatim_build_state_dir }}/binary.sha256" + owner: root + group: root + mode: "0644" + + always: + - name: Remove temporary Seriatim build directory + ansible.builtin.file: + path: "{{ seriatim_build_tempdir.path }}" + state: absent + when: seriatim_build_tempdir.path is defined diff --git a/tasks/main.yml b/tasks/main.yml index 328a2b7..fe3f3be 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -7,3 +7,6 @@ - name: Manage Seriatim source checkout ansible.builtin.import_tasks: source.yml + +- name: Build and install Seriatim + ansible.builtin.import_tasks: build.yml